@epoch-agent/runtime 0.1.0 → 0.2.0
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 +118 -70
- package/dist/index.d.ts +1532 -49
- package/dist/index.js +996 -1180
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { loadCommandDefinitions, createToolScope, expandCommand, RecentWorkspaces, clearModelCache, discoverModels, ScheduleStore, isWithinWindow, acquireScheduleLock, triggerPeriodMs, ScheduleRecorder, pruneRecordings, suggestRuleFromApproval, needsAllowlist, isNonInteractive, loadPlugins, assertLevelAllowed, discoverProject, ProviderRouter, ProviderError, PROVIDER_NO_CREDENTIAL, SessionManager, GoalService, MemoryManager, MemoryReviewer, resolveSkillDirs, SkillSystem, SkillLearner, HookManager, loadHookSources, ApprovalCache, PermissionManager, resolvePolicyDirs, resolvePoliciesDir, loadPolicyFiles, checkShadowing, compileRuleLists, ContextCompressor, compressionOptions, TrackerDB, getConfigOrigins, mapOperationType, affectedPaths, workspaceDisplayPath, persistParts, persistToolResults, ToolRegistry, createTodoTool, createGoalTool, createMemoryTool, createCodeExecTool, createAskQuestionTool, resolveWorkspace, unloadedInstructionFiles, resolveProjectRoot, loadConfig, CodeSandbox, CheckpointManager, createDelegateTool, loadRoleDefinitions, mergeRoles, builtinRoles, unknownRoleTools, BLOCKED_TOOLS, blockedRoleTools,
|
|
2
|
-
export { SANDBOX_COVERS, SANDBOX_EXCLUDES, SCHEDULE_DEFAULTS, SCHEDULE_DEFAULT_ALLOWLIST, nextRunAt, readRecording, skillIndexDescription } from '@epoch-agent/core';
|
|
3
|
-
import { projectCommandsDir, commandsDir,
|
|
1
|
+
import { loadCommandDefinitions, SLASH_COMMANDS_DIAG_MODULE, createToolScope, expandCommand, RecentWorkspaces, clearModelCache, discoverModels, ScheduleStore, isWithinWindow, acquireScheduleLock, triggerPeriodMs, ScheduleRecorder, pruneRecordings, suggestRuleFromApproval, needsAllowlist, isNonInteractive, loadPlugins, assertLevelAllowed, discoverProject, ProviderRouter, ProviderError, PROVIDER_NO_CREDENTIAL, SessionManager, GoalService, MemoryManager, MemoryReviewer, resolveSkillDirs, SkillSystem, SkillLearner, HookManager, loadHookSources, ApprovalCache, PermissionManager, resolvePolicyDirs, resolvePoliciesDir, loadPolicyFiles, checkShadowing, compileRuleLists, ContextCompressor, compressionOptions, TrackerDB, getConfigOrigins, mapOperationType, affectedPaths, workspaceDisplayPath, persistParts, persistToolResults, ToolRegistry, createTodoTool, createGoalTool, createMemoryTool, createCodeExecTool, createRunCodeTool, createSkillViewTool, createToolSearchTool, createToolRevealState, createAskQuestionTool, resolveWorkspace, unloadedInstructionFiles, resolveProjectRoot, loadConfig, CodeSandbox, CheckpointManager, createDelegateTool, findRole, resolveInWorkspace, listWorkspaceFiles, loadRoleDefinitions, mergeRoles, AGENT_ROLE_DIAG_MODULE, builtinRoles, unknownRoleTools, BLOCKED_TOOLS, blockedRoleTools, unknownRoleSkills, BudgetStore, mergeHeadlessPolicy, describeHeadlessPolicy, importSkills, previewSkillImport, removeSessionArtifacts, gateForTool, managedSwitchNotes, originOf, EpochConfigSchema, defaultConfig, SettingsFileSchema, isWritableSettingKey, writeSettingValue, SETTING_WRITE_APPLY, resolveSettings, SETTING_WRITE_LAYERS, readProviderKeyEnv, validateSchedule, createAllowAllGate, setTrustGate, TrustManager, createTrustGate, createDenyAllGate, getImportGate, setImportGate, createAllowAllImportGate, ImportTrustStore, createDenyAllImportGate, createImportGate, getCapability, restoreParts, createSessionSearchTools, PlanModeState, createPlanModeTools, toolTableTokens, explainSetting, settingWriteTargets, uninstallPlugin, readMarketplaces, searchMarketplaces, resolveEntrySource, resolveSource, readPluginRecords, createRoleFile, AgentLoop, resolveArtifactRetention, roleScopedProvider, createRoleScope, revealScopedProvider, withGoalPrompt, withGoalCompression, ScheduleRegistrar, resolveScheduleBackend, updateMarketplace, addMarketplace, SETTINGS_LAYER_ORDER, resolveMarketplaceRef, BudgetGuard, installPlugin, currentEpochVersion } from '@epoch-agent/core';
|
|
2
|
+
export { DEFAULT_GOAL_MAX_ROUNDS, MAX_GOAL_MAX_ROUNDS, MAX_OBJECTIVE_CHARS, MAX_REASON_CHARS, SANDBOX_COVERS, SANDBOX_EXCLUDES, SCHEDULE_DEFAULTS, SCHEDULE_DEFAULT_ALLOWLIST, nextRunAt, readRecording, skillIndexDescription } from '@epoch-agent/core';
|
|
3
|
+
import { projectCommandsDir, commandsDir, t, issueDetails, resolveHomeDir, configPath, readKey, setScalar, setSectionField, mcpConfigPath, workspacesPath, dbPath, automationLogsDir, pluginsStatePath, safeInit, memoriesDir, skillsDir, claudeUserSettingsPath, hooksPath, artifactsDir, resolveRipgrep, withTimeout, createProcessTable, marketplacesPath, pluginsDir, agentsDir, budgetStatePath, createSecretStore, setSecretStore, prefetchProviderSecrets, setSecretValues, migrateEnvSecrets, envPath, setDataKey, parseDataKey, generateDataKey, i18nDiagnostics, automationWorkDir, trustPath, trustedImportsPath, setShell, probeShell } from '@epoch-agent/infra';
|
|
4
4
|
export { t } from '@epoch-agent/infra';
|
|
5
5
|
import { isAbsolute, dirname, delimiter, resolve, join } from 'path';
|
|
6
|
-
import { AGENT_ROLE_NAME_PATTERN, isValidAgentRoleName, apiKeyEnvVar, getProviderInfo, PROVIDER_INFOS, parseModelRef, contentToText, systemNote, DiagnosticSink, METRIC, API_KEY_ENV_VARS, MCP_DATA_KEY_NAME, isPermissionLevel, NOOP_TELEMETRY, DEFAULT_AGENT_ROLE } from '@epoch-agent/protocol';
|
|
6
|
+
import { AGENT_ROLE_NAME_PATTERN, isValidAgentRoleName, apiKeyEnvVar, getProviderInfo, PROVIDER_INFOS, parseModelRef, contentToText, assistantTurnText, systemNote, ToolExposure, isMentionableFilePath, DiagnosticSink, METRIC, API_KEY_ENV_VARS, MCP_DATA_KEY_NAME, isPermissionLevel, NOOP_TELEMETRY, DEFAULT_AGENT_ROLE } from '@epoch-agent/protocol';
|
|
7
7
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync, renameSync, rmSync, appendFileSync } from 'fs';
|
|
8
|
-
import { parseMcpConfig, McpRegistry, McpAuthStore, mcpAuthStatus, loginToMcpServer, logoutFromMcpServer } from '@epoch-agent/plugin-mcp';
|
|
9
8
|
import { randomUUID, createHash } from 'crypto';
|
|
9
|
+
import { parseMcpConfig, McpRegistry, McpAuthStore, mcpAuthStatus, loginToMcpServer, logoutFromMcpServer } from '@epoch-agent/plugin-mcp';
|
|
10
10
|
import { filePlugin } from '@epoch-agent/plugin-file';
|
|
11
11
|
import { lspPlugin } from '@epoch-agent/plugin-lsp';
|
|
12
|
-
import { terminalPlugin,
|
|
12
|
+
import { terminalPlugin, rekeyTasks, listTasks } from '@epoch-agent/plugin-terminal';
|
|
13
13
|
export { taskOutput as backgroundTaskOutput, listTasks as listBackgroundTasks } from '@epoch-agent/plugin-terminal';
|
|
14
14
|
import { webPlugin, selectSearchProvider, createWebSearchTool } from '@epoch-agent/plugin-web';
|
|
15
15
|
|
|
@@ -53,18 +53,22 @@ function wireCommands(opts) {
|
|
|
53
53
|
});
|
|
54
54
|
if (summary.commands.length === 0 && summary.issues.length > 0) {
|
|
55
55
|
diags.warn(
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
SLASH_COMMANDS_DIAG_MODULE,
|
|
57
|
+
t("boot.commands_all_failed", { count: issueDetails(summary.issues).length }),
|
|
58
58
|
"no-command-loaded"
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
|
-
const available = new Set(toolProvider.list().map((
|
|
61
|
+
const available = new Set(toolProvider.list().map((t23) => t23.name));
|
|
62
62
|
for (const cmd of summary.commands) {
|
|
63
63
|
const unknown = (cmd.allowedTools ?? []).filter((name) => !available.has(name));
|
|
64
64
|
if (unknown.length > 0) {
|
|
65
65
|
diags.warn(
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
SLASH_COMMANDS_DIAG_MODULE,
|
|
67
|
+
(lang) => t(
|
|
68
|
+
"boot.command_allowed_tools",
|
|
69
|
+
{ name: `/${cmd.name}`, tools: unknown.join(", ") },
|
|
70
|
+
lang
|
|
71
|
+
),
|
|
68
72
|
"unknown-tool"
|
|
69
73
|
);
|
|
70
74
|
}
|
|
@@ -87,6 +91,65 @@ var init_commands = __esm({
|
|
|
87
91
|
"src/commands.ts"() {
|
|
88
92
|
}
|
|
89
93
|
});
|
|
94
|
+
// src/events.ts
|
|
95
|
+
function publishAgentEvent(sessionId, event) {
|
|
96
|
+
bus.publish(sessionId, event);
|
|
97
|
+
}
|
|
98
|
+
var ProcessEventBus, bus, processEventBus;
|
|
99
|
+
var init_events = __esm({
|
|
100
|
+
"src/events.ts"() {
|
|
101
|
+
ProcessEventBus = class {
|
|
102
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
103
|
+
failures = 0;
|
|
104
|
+
subscribe(fn) {
|
|
105
|
+
this.subscribers.add(fn);
|
|
106
|
+
let released = false;
|
|
107
|
+
return () => {
|
|
108
|
+
if (released) return;
|
|
109
|
+
released = true;
|
|
110
|
+
this.subscribers.delete(fn);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
get subscriberCount() {
|
|
114
|
+
return this.subscribers.size;
|
|
115
|
+
}
|
|
116
|
+
get deliveryFailures() {
|
|
117
|
+
return this.failures;
|
|
118
|
+
}
|
|
119
|
+
publish(sessionId, event) {
|
|
120
|
+
if (this.subscribers.size === 0) return;
|
|
121
|
+
for (const fn of this.subscribers) {
|
|
122
|
+
try {
|
|
123
|
+
fn(event, sessionId);
|
|
124
|
+
} catch {
|
|
125
|
+
this.failures += 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
bus = new ProcessEventBus();
|
|
131
|
+
processEventBus = bus;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
function dropOverlongToolNames(tools, module, code, diags) {
|
|
135
|
+
const kept = [];
|
|
136
|
+
for (const tool of tools) {
|
|
137
|
+
if (tool.name.length > MAX_TOOL_NAME_LENGTH) {
|
|
138
|
+
diags.warn(
|
|
139
|
+
module,
|
|
140
|
+
(lang) => t(
|
|
141
|
+
"host_capabilities.tool_name_too_long",
|
|
142
|
+
{ name: tool.name, limit: MAX_TOOL_NAME_LENGTH },
|
|
143
|
+
lang
|
|
144
|
+
),
|
|
145
|
+
code
|
|
146
|
+
);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
kept.push(tool);
|
|
150
|
+
}
|
|
151
|
+
return kept;
|
|
152
|
+
}
|
|
90
153
|
function wireHostCapabilities(caps, diags) {
|
|
91
154
|
if (!caps) return EMPTY;
|
|
92
155
|
const namespace = caps.namespace;
|
|
@@ -109,7 +172,8 @@ function wireHostCapabilities(caps, diags) {
|
|
|
109
172
|
const roles = wireRoles(caps.roles ?? [], namespace, diags);
|
|
110
173
|
const skillDirs = wireSkillDirs(caps.skillDirs ?? [], namespace, diags);
|
|
111
174
|
const mcpServers = wireMcpServers(caps.mcpServers ?? [], namespace, diags);
|
|
112
|
-
|
|
175
|
+
const tools = wireTools(caps.tools ?? [], namespace, diags);
|
|
176
|
+
if (roles.length > 0 || skillDirs.length > 0 || mcpServers.length > 0 || tools.length > 0) {
|
|
113
177
|
diags.ok(
|
|
114
178
|
HOST_CAPABILITIES_MODULE,
|
|
115
179
|
(lang) => t(
|
|
@@ -118,13 +182,14 @@ function wireHostCapabilities(caps, diags) {
|
|
|
118
182
|
namespace,
|
|
119
183
|
roles: roles.length,
|
|
120
184
|
skillDirs: skillDirs.length,
|
|
121
|
-
mcpServers: mcpServers.length
|
|
185
|
+
mcpServers: mcpServers.length,
|
|
186
|
+
tools: tools.length
|
|
122
187
|
},
|
|
123
188
|
lang
|
|
124
189
|
)
|
|
125
190
|
);
|
|
126
191
|
}
|
|
127
|
-
return { roles, skillDirs, mcpServers };
|
|
192
|
+
return { roles, skillDirs, mcpServers, tools };
|
|
128
193
|
}
|
|
129
194
|
function wireRoles(input, namespace, diags) {
|
|
130
195
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -153,7 +218,6 @@ function wireRoles(input, namespace, diags) {
|
|
|
153
218
|
...role.prompt ? { prompt: role.prompt } : {},
|
|
154
219
|
...role.tools ? { tools: [...role.tools] } : {},
|
|
155
220
|
...role.maxTurns ? { maxTurns: role.maxTurns } : {},
|
|
156
|
-
// 宿主说不出这一格(`HostAgentRole` 上没有它),这一行是它唯一的来源
|
|
157
221
|
source: "host"
|
|
158
222
|
});
|
|
159
223
|
}
|
|
@@ -199,11 +263,95 @@ function wireMcpServers(input, namespace, diags) {
|
|
|
199
263
|
}
|
|
200
264
|
return servers;
|
|
201
265
|
}
|
|
202
|
-
|
|
266
|
+
function wireTools(input, namespace, diags) {
|
|
267
|
+
const seen = /* @__PURE__ */ new Set();
|
|
268
|
+
const tools = [];
|
|
269
|
+
for (const tool of input) {
|
|
270
|
+
if (!HOST_TOOL_NAME_PATTERN.test(tool.name)) {
|
|
271
|
+
diags.warn(
|
|
272
|
+
HOST_CAPABILITIES_MODULE,
|
|
273
|
+
(lang) => t("host_capabilities.bad_tool_name", { name: tool.name }, lang),
|
|
274
|
+
"host-tool-invalid-name"
|
|
275
|
+
);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (seen.has(tool.name)) {
|
|
279
|
+
diags.warn(
|
|
280
|
+
HOST_CAPABILITIES_MODULE,
|
|
281
|
+
(lang) => t("host_capabilities.duplicate_tool", { name: tool.name }, lang),
|
|
282
|
+
"host-tool-duplicate"
|
|
283
|
+
);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
seen.add(tool.name);
|
|
287
|
+
const describeTarget = tool.describeTarget;
|
|
288
|
+
tools.push({
|
|
289
|
+
...tool,
|
|
290
|
+
name: `${namespace}__${tool.name}`,
|
|
291
|
+
execute: (args, ctx) => tool.execute(args, ctx),
|
|
292
|
+
...describeTarget ? { describeTarget: (args) => describeTarget.call(tool, args) } : {}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return tools;
|
|
296
|
+
}
|
|
297
|
+
var HOST_CAPABILITIES_MODULE, HOST_TOOL_NAME_PATTERN, MAX_TOOL_NAME_LENGTH, EMPTY;
|
|
203
298
|
var init_host_capabilities = __esm({
|
|
204
299
|
"src/host-capabilities.ts"() {
|
|
205
300
|
HOST_CAPABILITIES_MODULE = "HostCapabilities";
|
|
206
|
-
|
|
301
|
+
HOST_TOOL_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
302
|
+
MAX_TOOL_NAME_LENGTH = 128;
|
|
303
|
+
EMPTY = { roles: [], skillDirs: [], mcpServers: [], tools: [] };
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
async function wireHostMarketplaces(input) {
|
|
307
|
+
const { statePath, diagnostics: diags } = input;
|
|
308
|
+
const allowRemote = input.hostMarketplaces.allowRemote === true;
|
|
309
|
+
for (const raw of input.hostMarketplaces.sources) {
|
|
310
|
+
const source = raw.trim();
|
|
311
|
+
if (!source) {
|
|
312
|
+
diags.warn(HOST_MARKETPLACE_MODULE, (l) => t("plugin_control.market_empty", void 0, l));
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (!allowRemote && !isLocalSource(source)) {
|
|
316
|
+
diags.warn(
|
|
317
|
+
HOST_MARKETPLACE_MODULE,
|
|
318
|
+
(l) => t("plugin_control.market_remote_off", { source }, l),
|
|
319
|
+
"remote-disabled"
|
|
320
|
+
);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const outcome = await addOrRefresh(source, statePath);
|
|
324
|
+
if (!outcome.ok) {
|
|
325
|
+
diags.warn(
|
|
326
|
+
HOST_MARKETPLACE_MODULE,
|
|
327
|
+
(l) => t("plugin_control.market_failed", { source, reason: outcome.reason }, l),
|
|
328
|
+
"marketplace-unavailable"
|
|
329
|
+
);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
diags.ok(
|
|
333
|
+
HOST_MARKETPLACE_MODULE,
|
|
334
|
+
(l) => t("plugin_control.market_ready", { name: outcome.name, count: outcome.count }, l)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function isLocalSource(source) {
|
|
339
|
+
return resolveSource(source)?.type === "local";
|
|
340
|
+
}
|
|
341
|
+
async function addOrRefresh(source, statePath) {
|
|
342
|
+
try {
|
|
343
|
+
const existing = readMarketplaces(statePath).records.find((r) => r.source === source);
|
|
344
|
+
const outcome = existing ? await updateMarketplace(existing.name, { statePath }) : await addMarketplace(source, { statePath });
|
|
345
|
+
return outcome.ok ? { ok: true, name: outcome.record.name, count: outcome.record.catalog.plugins.length } : outcome;
|
|
346
|
+
} catch (err) {
|
|
347
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
348
|
+
return { ok: false, reason: message.split("\n")[0] ?? message };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
var HOST_MARKETPLACE_MODULE;
|
|
352
|
+
var init_host_marketplaces = __esm({
|
|
353
|
+
"src/host-marketplaces.ts"() {
|
|
354
|
+
HOST_MARKETPLACE_MODULE = "HostMarketplaces";
|
|
207
355
|
}
|
|
208
356
|
});
|
|
209
357
|
async function applyHostPreset(opts) {
|
|
@@ -441,6 +589,16 @@ var init_host_preset = __esm({
|
|
|
441
589
|
HOST_PRESET_MODULE = "HostPreset";
|
|
442
590
|
}
|
|
443
591
|
});
|
|
592
|
+
function providerLabel(info, lang) {
|
|
593
|
+
switch (info.type) {
|
|
594
|
+
case "ollama":
|
|
595
|
+
return t("provider.label_ollama", void 0, lang);
|
|
596
|
+
case "openai-compatible":
|
|
597
|
+
return t("provider.label_openai_compatible", void 0, lang);
|
|
598
|
+
default:
|
|
599
|
+
return info.label;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
444
602
|
function buildModelCatalogControl(opts) {
|
|
445
603
|
const { homeDir, configured } = opts;
|
|
446
604
|
const keyOf = (envVar) => {
|
|
@@ -453,7 +611,6 @@ function buildModelCatalogControl(opts) {
|
|
|
453
611
|
type: p.type,
|
|
454
612
|
label: p.label,
|
|
455
613
|
envVar: p.apiKeyEnv,
|
|
456
|
-
// 不需要 key 的那家(Ollama)恒 true —— 对它说「没配 key」是假话
|
|
457
614
|
hasKey: p.apiKeyEnv === null || keyOf(p.apiKeyEnv) !== ""
|
|
458
615
|
}));
|
|
459
616
|
},
|
|
@@ -488,6 +645,209 @@ var init_model_catalog = __esm({
|
|
|
488
645
|
"src/model-catalog.ts"() {
|
|
489
646
|
}
|
|
490
647
|
});
|
|
648
|
+
function createInstallFlow(deps) {
|
|
649
|
+
const locations = () => ({
|
|
650
|
+
pluginsDir: deps.pluginsDir,
|
|
651
|
+
statePath: deps.statePath,
|
|
652
|
+
epochVersion: currentEpochVersion()
|
|
653
|
+
});
|
|
654
|
+
return {
|
|
655
|
+
preview: async (source, extra, lang) => {
|
|
656
|
+
let seen;
|
|
657
|
+
const outcome = await installPlugin(source, {
|
|
658
|
+
...locations(),
|
|
659
|
+
...extra,
|
|
660
|
+
confirm: async (preview) => {
|
|
661
|
+
seen = preview;
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
if (seen) return { ok: true, preview: seen, token: previewToken(seen) };
|
|
666
|
+
return refuse(outcome, lang);
|
|
667
|
+
},
|
|
668
|
+
install: async (source, token, extra, lang) => {
|
|
669
|
+
let matched;
|
|
670
|
+
const outcome = await installPlugin(source, {
|
|
671
|
+
...locations(),
|
|
672
|
+
...extra,
|
|
673
|
+
confirm: async (preview) => {
|
|
674
|
+
matched = previewToken(preview) === token;
|
|
675
|
+
return matched;
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
if (outcome.ok) return { ok: true, record: outcome.record, preview: outcome.preview };
|
|
679
|
+
if (matched === false) {
|
|
680
|
+
return {
|
|
681
|
+
ok: false,
|
|
682
|
+
reason: "stale-token",
|
|
683
|
+
detail: t("plugin_control.stale_token", void 0, lang),
|
|
684
|
+
...outcome.preview ? { preview: outcome.preview } : {}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
return refuse(outcome, lang);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function replaceOpts(record) {
|
|
692
|
+
return {
|
|
693
|
+
replace: record.name,
|
|
694
|
+
...record.marketplace ? { marketplace: record.marketplace } : {}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function refuse(outcome, lang) {
|
|
698
|
+
if (outcome.ok) {
|
|
699
|
+
return {
|
|
700
|
+
ok: false,
|
|
701
|
+
reason: "failed",
|
|
702
|
+
detail: t("plugin_control.unexpected_commit", { name: outcome.record.name }, lang)
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
ok: false,
|
|
707
|
+
reason: outcome.preview?.conflict ? "conflict" : "failed",
|
|
708
|
+
detail: outcome.reason,
|
|
709
|
+
...outcome.preview ? { preview: outcome.preview } : {}
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function previewToken(preview) {
|
|
713
|
+
const inv = preview.inventory;
|
|
714
|
+
const hash = createHash("sha256");
|
|
715
|
+
hash.update(`${preview.manifest.name}@${preview.manifest.version}
|
|
716
|
+
`);
|
|
717
|
+
hash.update(`${preview.source.type} ${preview.source.raw}
|
|
718
|
+
`);
|
|
719
|
+
for (const list of [inv.commands, inv.roles, inv.skills, inv.mcpServers, inv.ignoredBuckets]) {
|
|
720
|
+
hash.update(`${[...list].sort().join(",")}
|
|
721
|
+
`);
|
|
722
|
+
}
|
|
723
|
+
for (const tally of [...inv.hooks].sort((a, b) => a.type.localeCompare(b.type))) {
|
|
724
|
+
hash.update(`${tally.type}=${tally.count};`);
|
|
725
|
+
}
|
|
726
|
+
hash.update(`
|
|
727
|
+
${inv.denyRules}|${inv.jsTools ? 1 : 0}
|
|
728
|
+
`);
|
|
729
|
+
hash.update(`${preview.versionUnmet?.required ?? "-"}
|
|
730
|
+
`);
|
|
731
|
+
return hash.digest("hex").slice(0, 32);
|
|
732
|
+
}
|
|
733
|
+
var init_plugin_install = __esm({
|
|
734
|
+
"src/plugin-install.ts"() {
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
function createPluginControl(deps) {
|
|
738
|
+
let pendingRestart = false;
|
|
739
|
+
const live = new Map(deps.loaded.map((p) => [p.name, p.counts]));
|
|
740
|
+
const flow = createInstallFlow({ pluginsDir: deps.pluginsDir, statePath: deps.statePath });
|
|
741
|
+
const mark = (outcome) => {
|
|
742
|
+
if (outcome.ok) pendingRestart = true;
|
|
743
|
+
return outcome;
|
|
744
|
+
};
|
|
745
|
+
const resolveRef = (ref, lang) => {
|
|
746
|
+
const hit = resolveMarketplaceRef(ref, deps.marketplacesPath);
|
|
747
|
+
if (!hit) {
|
|
748
|
+
return {
|
|
749
|
+
ok: false,
|
|
750
|
+
reason: "unknown-ref",
|
|
751
|
+
detail: t("plugin_control.not_a_ref", { ref }, lang)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
if (!hit.ok) return { ok: false, reason: "unknown-ref", detail: hit.reason };
|
|
755
|
+
if (!deps.allowRemote && !isLocalSource(hit.ref.source)) {
|
|
756
|
+
return {
|
|
757
|
+
ok: false,
|
|
758
|
+
reason: "remote-disabled",
|
|
759
|
+
detail: t("plugin_control.entry_remote_off", { ref, source: hit.ref.source }, lang)
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
return { ok: true, source: hit.ref.source, marketplace: hit.ref.marketplace };
|
|
763
|
+
};
|
|
764
|
+
const recordOf = (name, lang) => {
|
|
765
|
+
const record = readPluginRecords(deps.statePath).records.find((r) => r.name === name);
|
|
766
|
+
if (!record) {
|
|
767
|
+
return {
|
|
768
|
+
ok: false,
|
|
769
|
+
reason: "not-installed",
|
|
770
|
+
detail: t("plugin_control.not_installed", { name }, lang)
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
if (record.linked) {
|
|
774
|
+
return {
|
|
775
|
+
ok: false,
|
|
776
|
+
reason: "nothing-to-do",
|
|
777
|
+
detail: t("plugin_control.linked_no_update", { name, source: record.source }, lang)
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (!deps.allowRemote && !isLocalSource(record.source)) {
|
|
781
|
+
return {
|
|
782
|
+
ok: false,
|
|
783
|
+
reason: "remote-disabled",
|
|
784
|
+
detail: t("plugin_control.update_remote_off", { name, source: record.source }, lang)
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
return { ok: true, record };
|
|
788
|
+
};
|
|
789
|
+
return {
|
|
790
|
+
list: () => readPluginRecords(deps.statePath).records.map((r) => ({
|
|
791
|
+
...r,
|
|
792
|
+
active: live.has(r.name),
|
|
793
|
+
...live.get(r.name) ? { counts: live.get(r.name) } : {}
|
|
794
|
+
})),
|
|
795
|
+
search: (keyword = "") => {
|
|
796
|
+
const markets = new Map(
|
|
797
|
+
readMarketplaces(deps.marketplacesPath).records.map((r) => [r.name, r.source])
|
|
798
|
+
);
|
|
799
|
+
return searchMarketplaces(keyword, deps.marketplacesPath).map((hit) => {
|
|
800
|
+
const market = markets.get(hit.marketplace);
|
|
801
|
+
const absolute = market === void 0 ? void 0 : resolveEntrySource(hit.entry.source, market);
|
|
802
|
+
const kind = absolute === void 0 ? void 0 : resolveSource(absolute)?.type;
|
|
803
|
+
return {
|
|
804
|
+
marketplace: hit.marketplace,
|
|
805
|
+
entry: hit.entry,
|
|
806
|
+
ref: hit.ref,
|
|
807
|
+
...kind ? { sourceType: kind } : {},
|
|
808
|
+
installable: deps.allowRemote ? kind !== void 0 : kind === "local"
|
|
809
|
+
};
|
|
810
|
+
});
|
|
811
|
+
},
|
|
812
|
+
preview: async (ref, lang) => {
|
|
813
|
+
const target = resolveRef(ref, lang);
|
|
814
|
+
return target.ok ? flow.preview(target.source, { marketplace: target.marketplace }, lang) : target;
|
|
815
|
+
},
|
|
816
|
+
install: async (ref, token, lang) => {
|
|
817
|
+
const target = resolveRef(ref, lang);
|
|
818
|
+
return target.ok ? mark(await flow.install(target.source, token, { marketplace: target.marketplace }, lang)) : target;
|
|
819
|
+
},
|
|
820
|
+
previewUpdate: async (name, lang) => {
|
|
821
|
+
const found = recordOf(name, lang);
|
|
822
|
+
return found.ok ? flow.preview(found.record.source, replaceOpts(found.record), lang) : found;
|
|
823
|
+
},
|
|
824
|
+
update: async (name, token, lang) => {
|
|
825
|
+
const found = recordOf(name, lang);
|
|
826
|
+
return found.ok ? mark(await flow.install(found.record.source, token, replaceOpts(found.record), lang)) : found;
|
|
827
|
+
},
|
|
828
|
+
uninstall: async (name, lang) => {
|
|
829
|
+
const outcome = await uninstallPlugin(name, { statePath: deps.statePath });
|
|
830
|
+
if (!outcome.ok) {
|
|
831
|
+
return {
|
|
832
|
+
ok: false,
|
|
833
|
+
reason: "not-installed",
|
|
834
|
+
detail: t("plugin_control.not_installed", { name }, lang)
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
pendingRestart = true;
|
|
838
|
+
return { ok: true, detail: outcome.message };
|
|
839
|
+
},
|
|
840
|
+
get pendingRestart() {
|
|
841
|
+
return pendingRestart;
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
var init_plugin_control = __esm({
|
|
846
|
+
"src/plugin-control.ts"() {
|
|
847
|
+
init_host_marketplaces();
|
|
848
|
+
init_plugin_install();
|
|
849
|
+
}
|
|
850
|
+
});
|
|
491
851
|
function wirePluginMcpServers(mcpPaths, diags) {
|
|
492
852
|
const out = [];
|
|
493
853
|
for (const ref of mcpPaths) {
|
|
@@ -550,9 +910,6 @@ function createRoleWriteControl(deps) {
|
|
|
550
910
|
return {
|
|
551
911
|
ok: false,
|
|
552
912
|
reason: "duplicate",
|
|
553
|
-
// 把**哪一层**占着这个名字说出来:内置和项目级的出路不同(前者只能换名字,
|
|
554
|
-
// 后者他还可以去改自己仓库里那一份)。只说「重名了」的话,一个撞上
|
|
555
|
-
// `general` 的人会以为自己的 `~/.epoch/agents/` 里躺着一份看不见的文件
|
|
556
913
|
detail: t(
|
|
557
914
|
"role_write.dup_in_table",
|
|
558
915
|
{ name, layer: t(LAYER_KEY[clash.source], void 0, lang) },
|
|
@@ -588,6 +945,7 @@ function createRoleWriteControl(deps) {
|
|
|
588
945
|
description: input.description,
|
|
589
946
|
...input.prompt === void 0 ? {} : { prompt: input.prompt },
|
|
590
947
|
...input.tools === void 0 ? {} : { tools: input.tools },
|
|
948
|
+
...input.skills === void 0 ? {} : { skills: input.skills },
|
|
591
949
|
...input.maxTurns === void 0 ? {} : { maxTurns: input.maxTurns }
|
|
592
950
|
},
|
|
593
951
|
{ dir: deps.userDir, source: "user" }
|
|
@@ -597,10 +955,6 @@ function createRoleWriteControl(deps) {
|
|
|
597
955
|
return {
|
|
598
956
|
ok: true,
|
|
599
957
|
path: written.path,
|
|
600
|
-
// 回**重载之后那张表**里的那一份,不是 core 刚从文件里读出来的那个:
|
|
601
|
-
// 两者在 `tools` 上真会不同(`mergeRoles` 的交集语义)。撞名已经在上面
|
|
602
|
-
// 拦掉了,所以今天这两份恒等 —— `??` 那一支是给「没有 provider、
|
|
603
|
-
// 角色表恒空」的那种进程用的,那时表里找不到它,而文件确实写了
|
|
604
958
|
role: findRole(deps.roles(), name) ?? written.role
|
|
605
959
|
};
|
|
606
960
|
}
|
|
@@ -645,32 +999,11 @@ var ApprovalRelay, QuestionRelay;
|
|
|
645
999
|
var init_serialize = __esm({
|
|
646
1000
|
"src/serialize.ts"() {
|
|
647
1001
|
ApprovalRelay = class {
|
|
648
|
-
/**
|
|
649
|
-
* `requestId` → 请求内容 + 答复闭包 + 登记那一刻。
|
|
650
|
-
*
|
|
651
|
-
* 存了 `request` 而不只是闭包,是为了 `listPending()` —— 见它的注释。
|
|
652
|
-
* Map 的插入序 = 请求发生的时间序,`listPending()` 靠它给出稳定顺序。
|
|
653
|
-
*
|
|
654
|
-
* ⚠️ **`since` 是 2026-08-16 补的,在那之前这张表「顺序有了、时刻没有」**:
|
|
655
|
-
* 插入序答得出「谁先谁后」,答不出「第一条是几点挂上的」,而界面上那一格要说
|
|
656
|
-
* 「已等多久」只能问后者。判据在 {@link ApprovalRelay.pendingSince} 上。
|
|
657
|
-
*/
|
|
658
1002
|
pendingMap = /* @__PURE__ */ new Map();
|
|
659
|
-
/** 登记时刻从哪儿读。见 {@link RelayClock} */
|
|
660
1003
|
clock;
|
|
661
1004
|
constructor(clock = Date.now) {
|
|
662
1005
|
this.clock = clock;
|
|
663
1006
|
}
|
|
664
|
-
/**
|
|
665
|
-
* 登记一个刚发给 renderer 的审批。
|
|
666
|
-
*
|
|
667
|
-
* 直接吃 `toSerializable()` 的产出:非审批事件(没有 `respond`)传进来是空操作,
|
|
668
|
-
* 于是宿主的消费循环里不用先判类型。
|
|
669
|
-
*
|
|
670
|
-
* ⚠️ 时钟**只在真的要插一条时才读**(在 `return` 后面)。写在函数头上的话,
|
|
671
|
-
* 每一条流过的事件都会读一次挂钟 —— 而这张表是被 `serializeStream()` 就着
|
|
672
|
-
* **每一个**事件调的,注入过时钟的宿主那边会当场看见自己的脚本被走空。
|
|
673
|
-
*/
|
|
674
1007
|
track(serialized) {
|
|
675
1008
|
const { event, respond } = serialized;
|
|
676
1009
|
if (event.type !== "approval-request" || !respond) return;
|
|
@@ -680,13 +1013,6 @@ var init_serialize = __esm({
|
|
|
680
1013
|
since: this.clock()
|
|
681
1014
|
});
|
|
682
1015
|
}
|
|
683
|
-
/**
|
|
684
|
-
* 答复。
|
|
685
|
-
*
|
|
686
|
-
* @returns 是否真的接回了引擎。`false` 意味着这个 `requestId` 不认识 ——
|
|
687
|
-
* 重复答复、或者 renderer 拿着一个上一轮的 ID 回来了。宿主该把它
|
|
688
|
-
* 当成日志而不是错误:用户在窗口上双击一下审批按钮就会走到这里。
|
|
689
|
-
*/
|
|
690
1016
|
respond(requestId, outcome, note) {
|
|
691
1017
|
const pending = this.pendingMap.get(requestId);
|
|
692
1018
|
if (!pending) return false;
|
|
@@ -694,52 +1020,13 @@ var init_serialize = __esm({
|
|
|
694
1020
|
pending.respond(outcome, note);
|
|
695
1021
|
return true;
|
|
696
1022
|
}
|
|
697
|
-
/** 还有多少个审批挂在 renderer 那边没回来 */
|
|
698
1023
|
get pendingCount() {
|
|
699
1024
|
return this.pendingMap.size;
|
|
700
1025
|
}
|
|
701
|
-
/**
|
|
702
|
-
* **最早**那一条还挂着的审批是什么时候登记的(epoch 毫秒);一条都没挂着时 null。
|
|
703
|
-
*
|
|
704
|
-
* 存在的理由只有一个:界面上要说得出「**已等**多久」。`pendingCount` 答的是
|
|
705
|
-
* 「有几条」,而那一格问的是「等了多久」—— 两者之间差的正是这个时刻,
|
|
706
|
-
* 而在它之前网线上**没有任何一个字段答得出来**(全文在
|
|
707
|
-
* `WireSessionSummary.pendingSince` 上,包括「为什么 `updatedAt` 顶不了它」)。
|
|
708
|
-
*
|
|
709
|
-
* ## 为什么是**最早**那一条,不是最近那一条
|
|
710
|
-
*
|
|
711
|
-
* 消费它的那一格挑的是「晾得最久的那个会话」。报最近那一条的话,一个已经卡了
|
|
712
|
-
* 两小时、刚刚又多挂上一条的会话会说「已等 3 秒」—— 屏幕上那个数会和它被挑中
|
|
713
|
-
* 的理由互相打脸。「最早」还有一条硬性质:它**只在这个会话彻底不卡了之后才动**
|
|
714
|
-
* (表清空 → null),而「最近」每多挂一条就往前跳一次,读起来像是等待被重置了。
|
|
715
|
-
*
|
|
716
|
-
* ## 取的是表里第一条,靠的正是上面那条插入序不变量
|
|
717
|
-
*
|
|
718
|
-
* `pendingMap` 的插入序 = 请求发生的时间序(见它自己的注释),所以第一条就是
|
|
719
|
-
* 最早那一条,答完出账之后自然轮到下一条。**刻意不遍历取 `Math.min`**:
|
|
720
|
-
* 那等于给「最早」立第二个定义,而两个定义哪天对不上时,症状是屏幕上一个
|
|
721
|
-
* 说不出来历的数字 —— 没有任何一处会红。
|
|
722
|
-
*
|
|
723
|
-
* **只读**:这张表的时刻由 `track()` 一处写死,宿主不许改。一个能被外面拨的
|
|
724
|
-
* 「已等多久」等于让每个宿主自己发明一套等待口径。
|
|
725
|
-
*/
|
|
726
1026
|
get pendingSince() {
|
|
727
1027
|
const first = this.pendingMap.values().next();
|
|
728
1028
|
return first.done ? null : first.value.since;
|
|
729
1029
|
}
|
|
730
|
-
/**
|
|
731
|
-
* 列出还挂着的审批,按发生的先后顺序。
|
|
732
|
-
*
|
|
733
|
-
* 给「**新连上来的宿主要补拉**」用:浏览器刷新一次页面,SSE 是新连接,
|
|
734
|
-
* 之前推过去的 `approval-request` 就没人接了 —— 而引擎那侧还阻塞在等答复。
|
|
735
|
-
* 没有这个方法,新页面只能干等到超时(对标 OpenCode 的
|
|
736
|
-
* `session.permission.list`,`packages/server/src/handlers/permission.ts`)。
|
|
737
|
-
*
|
|
738
|
-
* 返回的形状**与 SSE 上推的那一条完全一致**(`SerializableApprovalRequest`),
|
|
739
|
-
* 于是客户端的「收到一条审批」只有一条代码路径,不用为补拉再写一份解析。
|
|
740
|
-
*
|
|
741
|
-
* `pendingCount` 不能替代它:数字告诉你有几条挂着,但补拉需要的是内容。
|
|
742
|
-
*/
|
|
743
1030
|
listPending() {
|
|
744
1031
|
return [...this.pendingMap].map(([requestId, { request }]) => ({
|
|
745
1032
|
type: "approval-request",
|
|
@@ -747,16 +1034,6 @@ var init_serialize = __esm({
|
|
|
747
1034
|
request
|
|
748
1035
|
}));
|
|
749
1036
|
}
|
|
750
|
-
/**
|
|
751
|
-
* 界面没了:把挂起的审批全部**放弃**,返回放弃的个数。
|
|
752
|
-
*
|
|
753
|
-
* 刻意**不** resolve 任何 promise —— 那会变成一串「用户拒绝」。调用方拿到非 0
|
|
754
|
-
* 返回值后该做的是 `abort()` 掉这一轮(`session.run(msg, { signal })` 的那个
|
|
755
|
-
* signal):流一结束,`AgentSession` 的 finally 会把剩下的请求兜底成 deny,
|
|
756
|
-
* 引擎不会永久挂住,而模型看到的是「这一轮被中止了」而不是「用户否了我五次」。
|
|
757
|
-
*
|
|
758
|
-
* 接法见 [docs/EMBEDDING.md](../../../docs/EMBEDDING.md) 的「窗口关掉了怎么办」。
|
|
759
|
-
*/
|
|
760
1037
|
abandon() {
|
|
761
1038
|
const count = this.pendingMap.size;
|
|
762
1039
|
this.pendingMap.clear();
|
|
@@ -764,24 +1041,11 @@ var init_serialize = __esm({
|
|
|
764
1041
|
}
|
|
765
1042
|
};
|
|
766
1043
|
QuestionRelay = class {
|
|
767
|
-
/**
|
|
768
|
-
* `requestId` → 请求内容 + 答复闭包 + 登记那一刻。
|
|
769
|
-
* Map 的插入序 = 提问发生的时间序。`since` 见 {@link QuestionRelay.pendingSince}
|
|
770
|
-
*/
|
|
771
1044
|
pendingMap = /* @__PURE__ */ new Map();
|
|
772
|
-
/** 登记时刻从哪儿读。见 {@link RelayClock} */
|
|
773
1045
|
clock;
|
|
774
1046
|
constructor(clock = Date.now) {
|
|
775
1047
|
this.clock = clock;
|
|
776
1048
|
}
|
|
777
|
-
/**
|
|
778
|
-
* 登记一个刚发给宿主的提问。
|
|
779
|
-
*
|
|
780
|
-
* 直接吃 `toSerializable()` 的产出:非提问事件传进来是空操作,
|
|
781
|
-
* 于是宿主的消费循环里不用先判类型(同 `ApprovalRelay.track`)。
|
|
782
|
-
*
|
|
783
|
-
* 时钟同样只在真要插一条时才读,理由逐字同 `ApprovalRelay.track`。
|
|
784
|
-
*/
|
|
785
1049
|
track(serialized) {
|
|
786
1050
|
const { event, respondQuestion } = serialized;
|
|
787
1051
|
if (event.type !== "question" || !respondQuestion) return;
|
|
@@ -791,13 +1055,6 @@ var init_serialize = __esm({
|
|
|
791
1055
|
since: this.clock()
|
|
792
1056
|
});
|
|
793
1057
|
}
|
|
794
|
-
/**
|
|
795
|
-
* 答复。
|
|
796
|
-
*
|
|
797
|
-
* @returns 是否真的接回了引擎。`false` = 这个 `requestId` 不认识
|
|
798
|
-
* (重复答复、上一轮的 ID、或者已经被 `abandon()` 掉了)。
|
|
799
|
-
* 调用方该把它当日志而不是错误。
|
|
800
|
-
*/
|
|
801
1058
|
respond(requestId, answer) {
|
|
802
1059
|
const pending = this.pendingMap.get(requestId);
|
|
803
1060
|
if (!pending) return false;
|
|
@@ -805,25 +1062,13 @@ var init_serialize = __esm({
|
|
|
805
1062
|
pending.respond(answer);
|
|
806
1063
|
return true;
|
|
807
1064
|
}
|
|
808
|
-
/** 还有多少个提问挂在宿主那边没回来 */
|
|
809
1065
|
get pendingCount() {
|
|
810
1066
|
return this.pendingMap.size;
|
|
811
1067
|
}
|
|
812
|
-
/**
|
|
813
|
-
* **最早**那一条还挂着的提问是什么时候登记的;一条都没挂着时 null。
|
|
814
|
-
*
|
|
815
|
-
* 与 {@link ApprovalRelay.pendingSince} 逐字同构、同三条判据(最早不是最近、
|
|
816
|
-
* 取表里第一条、只读)。写在那一个上,别在这儿再判一遍。
|
|
817
|
-
*
|
|
818
|
-
* ⚠️ **两张表各给一个,网线上却只有一格**:合并成「这个会话从什么时候起被拦住」
|
|
819
|
-
* 是消费方(Hub)的事,不是这一层的 —— 这一层只知道自己这张表。
|
|
820
|
-
* 判据在 `WireSessionSummary.pendingSince` 的第一节。
|
|
821
|
-
*/
|
|
822
1068
|
get pendingSince() {
|
|
823
1069
|
const first = this.pendingMap.values().next();
|
|
824
1070
|
return first.done ? null : first.value.since;
|
|
825
1071
|
}
|
|
826
|
-
/** 列出还挂着的提问,按发生的先后顺序。给「新连上来的宿主要补拉」用(验收 9) */
|
|
827
1072
|
listPending() {
|
|
828
1073
|
return [...this.pendingMap].map(([requestId, { request }]) => ({
|
|
829
1074
|
type: "question",
|
|
@@ -831,13 +1076,6 @@ var init_serialize = __esm({
|
|
|
831
1076
|
request
|
|
832
1077
|
}));
|
|
833
1078
|
}
|
|
834
|
-
/**
|
|
835
|
-
* 界面没了:把挂起的提问全部**放弃**,返回放弃的个数(验收 8)。
|
|
836
|
-
*
|
|
837
|
-
* 刻意**不** resolve 任何 promise —— 那会变成一串编出来的答案。调用方拿到非 0
|
|
838
|
-
* 返回值后该 `abort()` 掉这一轮:流一结束,`AgentSession` 的 finally 会把剩下的
|
|
839
|
-
* 请求兜底成 `skipped`(那是实话:确实没人答),引擎不会永久挂住。
|
|
840
|
-
*/
|
|
841
1079
|
abandon() {
|
|
842
1080
|
const count = this.pendingMap.size;
|
|
843
1081
|
this.pendingMap.clear();
|
|
@@ -947,12 +1185,7 @@ async function runOnce(store, def, opts, startedAt, now) {
|
|
|
947
1185
|
const runtime = await build({
|
|
948
1186
|
homeDir: opts.homeDir,
|
|
949
1187
|
workDir: def.workDir,
|
|
950
|
-
// **显式 false**:不靠 TTY 探测。OS 调度器起的进程天然没有 TTY,
|
|
951
|
-
// 但嵌入宿主那条路(`headless.js` 由宿主起)可能有,而那时按交互处理
|
|
952
|
-
// 会让整个「没人可问」的语义塌掉 —— 预授权清单不生效、审批挂着等一个不在的人
|
|
953
1188
|
interactive: false,
|
|
954
|
-
// 任务级清单是**取代**不是叠加:一个任务的授权面必须只由任务自己说清,
|
|
955
|
-
// 否则用户改一次 `~/.epoch/config.yaml` 就静默放宽了全部十个定时任务
|
|
956
1189
|
headless: { allowTools: [...def.allowTools], allowOperations: [...def.allowOperations] },
|
|
957
1190
|
sessionSource: "schedule",
|
|
958
1191
|
installSignalHandlers: true
|
|
@@ -1137,37 +1370,50 @@ var init_executor = __esm({
|
|
|
1137
1370
|
}
|
|
1138
1371
|
});
|
|
1139
1372
|
function createScheduleControl(opts) {
|
|
1140
|
-
|
|
1373
|
+
let opened = null;
|
|
1374
|
+
let openError = null;
|
|
1375
|
+
try {
|
|
1376
|
+
opened = new ScheduleStore(dbPath(opts.homeDir));
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
openError = err instanceof Error ? err : new Error(String(err));
|
|
1379
|
+
}
|
|
1380
|
+
const store = () => {
|
|
1381
|
+
if (!opened) {
|
|
1382
|
+
throw new Error(
|
|
1383
|
+
`schedule store unavailable: ${openError?.message ?? "unknown"} (see ScheduleControl.storeAvailable)`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
return opened;
|
|
1387
|
+
};
|
|
1141
1388
|
let backendPromise = null;
|
|
1142
1389
|
const backendOf = () => {
|
|
1143
1390
|
backendPromise ??= resolveScheduleBackend(opts.platform ?? process.platform);
|
|
1144
1391
|
return backendPromise;
|
|
1145
1392
|
};
|
|
1146
1393
|
const registrarOf = async () => new ScheduleRegistrar({
|
|
1147
|
-
store,
|
|
1394
|
+
store: store(),
|
|
1148
1395
|
backend: await backendOf(),
|
|
1149
1396
|
homeDir: opts.homeDir,
|
|
1150
1397
|
...opts.runner ? { runner: opts.runner } : {}
|
|
1151
|
-
// ⚠️ **刻意不给 `cli`**:这一层不知道宿主怎么被唤起,猜的代价见文件头
|
|
1152
1398
|
});
|
|
1153
1399
|
const save = async (def, register) => {
|
|
1154
1400
|
if (!register) return { schedule: def, issues: [] };
|
|
1155
1401
|
const registrar = await registrarOf();
|
|
1156
1402
|
const registration = await registrar.register(def);
|
|
1157
|
-
return { schedule: store.get(def.id) ?? def, issues: [], registration };
|
|
1403
|
+
return { schedule: store().get(def.id) ?? def, issues: [], registration };
|
|
1158
1404
|
};
|
|
1159
1405
|
const control = {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1406
|
+
storeAvailable: opened !== null,
|
|
1407
|
+
list: () => store().list(),
|
|
1408
|
+
get: (id) => store().get(id),
|
|
1409
|
+
runs: (id, limit) => store().listRuns(id, limit),
|
|
1410
|
+
recentRuns: (limit) => store().listRecentRuns(limit),
|
|
1164
1411
|
capability: async () => {
|
|
1165
1412
|
const backend = await backendOf();
|
|
1166
1413
|
return {
|
|
1167
1414
|
backend: backend?.kind ?? null,
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
canRegister: backend !== null && opts.runner !== void 0
|
|
1415
|
+
canRegister: backend !== null && opts.runner !== void 0,
|
|
1416
|
+
storeAvailable: opened !== null
|
|
1171
1417
|
};
|
|
1172
1418
|
},
|
|
1173
1419
|
create: async (input, saveOpts) => {
|
|
@@ -1188,12 +1434,12 @@ function createScheduleControl(opts) {
|
|
|
1188
1434
|
homeDir: opts.homeDir
|
|
1189
1435
|
}).issues;
|
|
1190
1436
|
if (issues.length > 0) return { issues };
|
|
1191
|
-
const created = store.create({ ...input, workDir: workDir || opts.homeDir });
|
|
1192
|
-
const def = workDir ? created : store.update(created.id, { workDir: automationWorkDir(created.id, opts.homeDir) }) ?? created;
|
|
1437
|
+
const created = store().create({ ...input, workDir: workDir || opts.homeDir });
|
|
1438
|
+
const def = workDir ? created : store().update(created.id, { workDir: automationWorkDir(created.id, opts.homeDir) }) ?? created;
|
|
1193
1439
|
return save(def, saveOpts?.register !== false);
|
|
1194
1440
|
},
|
|
1195
1441
|
update: async (id, patch, saveOpts) => {
|
|
1196
|
-
const current = store.get(id);
|
|
1442
|
+
const current = store().get(id);
|
|
1197
1443
|
if (!current) return { issues: [] };
|
|
1198
1444
|
const merged = { ...current, ...patch };
|
|
1199
1445
|
const issues = validateSchedule({
|
|
@@ -1212,18 +1458,18 @@ function createScheduleControl(opts) {
|
|
|
1212
1458
|
homeDir: opts.homeDir
|
|
1213
1459
|
}).issues;
|
|
1214
1460
|
if (issues.length > 0) return { issues };
|
|
1215
|
-
const next = store.update(id, patch);
|
|
1461
|
+
const next = store().update(id, patch);
|
|
1216
1462
|
if (!next) return { issues: [] };
|
|
1217
1463
|
return save(next, saveOpts?.register !== false);
|
|
1218
1464
|
},
|
|
1219
1465
|
remove: async (id) => {
|
|
1220
|
-
const def = store.get(id);
|
|
1466
|
+
const def = store().get(id);
|
|
1221
1467
|
if (!def) return false;
|
|
1222
1468
|
if (def.osTaskId) await (await registrarOf()).unregister(def);
|
|
1223
|
-
return store.remove(id);
|
|
1469
|
+
return store().remove(id);
|
|
1224
1470
|
},
|
|
1225
1471
|
setEnabled: async (id, enabled) => {
|
|
1226
|
-
const def = store.get(id);
|
|
1472
|
+
const def = store().get(id);
|
|
1227
1473
|
if (!def) return { ok: false, reason: "backend-error", detail: "schedule-not-found" };
|
|
1228
1474
|
return (await registrarOf()).setEnabled(def, enabled);
|
|
1229
1475
|
},
|
|
@@ -1235,7 +1481,7 @@ function createScheduleControl(opts) {
|
|
|
1235
1481
|
}),
|
|
1236
1482
|
needsAllowlist
|
|
1237
1483
|
};
|
|
1238
|
-
return { control, dispose: () =>
|
|
1484
|
+
return { control, dispose: () => opened?.close(), openError };
|
|
1239
1485
|
}
|
|
1240
1486
|
var init_control = __esm({
|
|
1241
1487
|
"src/schedule/control.ts"() {
|
|
@@ -1244,7 +1490,7 @@ var init_control = __esm({
|
|
|
1244
1490
|
});
|
|
1245
1491
|
function resolvePermissionLevel(raw, diags) {
|
|
1246
1492
|
if (isPermissionLevel(raw)) return raw;
|
|
1247
|
-
diags.warn("Perm",
|
|
1493
|
+
diags.warn("Perm", t("boot.bad_permission_level", { raw }), "bad-permission-level");
|
|
1248
1494
|
return "default";
|
|
1249
1495
|
}
|
|
1250
1496
|
function resolveModelCapability(config, diags) {
|
|
@@ -1258,7 +1504,7 @@ function resolveModelCapability(config, diags) {
|
|
|
1258
1504
|
} catch {
|
|
1259
1505
|
diags.warn(
|
|
1260
1506
|
"Capability",
|
|
1261
|
-
|
|
1507
|
+
t("boot.no_model_metadata", { model: config.model }),
|
|
1262
1508
|
"no-model-metadata"
|
|
1263
1509
|
);
|
|
1264
1510
|
return { contextLength: config.contextLength, supportsImages: false };
|
|
@@ -1266,21 +1512,26 @@ function resolveModelCapability(config, diags) {
|
|
|
1266
1512
|
}
|
|
1267
1513
|
function reportUtilityModel(info, diags) {
|
|
1268
1514
|
if (!info.configured) {
|
|
1269
|
-
diags.skipped("
|
|
1515
|
+
diags.skipped("UtilityModel", (lang) => t("boot.utility_model_none", void 0, lang));
|
|
1270
1516
|
return;
|
|
1271
1517
|
}
|
|
1272
1518
|
if (info.issue) {
|
|
1273
|
-
diags.warn("
|
|
1519
|
+
diags.warn("UtilityModel", info.issue, "bad-model-ref");
|
|
1274
1520
|
return;
|
|
1275
1521
|
}
|
|
1276
|
-
diags.ok(
|
|
1522
|
+
diags.ok(
|
|
1523
|
+
"UtilityModel",
|
|
1524
|
+
(lang) => t(
|
|
1525
|
+
"boot.utility_model_selected",
|
|
1526
|
+
{ provider: info.selection.provider, model: info.selection.model },
|
|
1527
|
+
lang
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1277
1530
|
}
|
|
1278
1531
|
function buildTrust(config, workDir, diags, interactive) {
|
|
1279
1532
|
const root = resolveProjectRoot(workDir);
|
|
1280
1533
|
if (config.trust?.enabled === false) {
|
|
1281
|
-
const gate2 = createAllowAllGate(
|
|
1282
|
-
"\u4FE1\u4EFB\u95F8\u95E8\u5DF2\u5173\u95ED\uFF08config.trust.enabled=false\uFF09\uFF1A\u4EFB\u4F55\u76EE\u5F55\u7684\u9879\u76EE\u6307\u4EE4\u90FD\u4F1A\u52A0\u8F7D"
|
|
1283
|
-
);
|
|
1534
|
+
const gate2 = createAllowAllGate(t("boot.trust_disabled"));
|
|
1284
1535
|
setTrustGate(gate2);
|
|
1285
1536
|
const trust2 = gate2.decide(workDir);
|
|
1286
1537
|
diags.warn("Trust", trust2.reason, "trust-disabled");
|
|
@@ -1290,7 +1541,7 @@ function buildTrust(config, workDir, diags, interactive) {
|
|
|
1290
1541
|
}
|
|
1291
1542
|
const manager = safeInit("Trust", () => new TrustManager(trustPath(config.homeDir)), diags);
|
|
1292
1543
|
for (const err of manager?.loadErrors ?? []) diags.warn("Trust", err, "trust-load-error");
|
|
1293
|
-
const gate = manager ? createTrustGate({ manager, interactive }) : createDenyAllGate("
|
|
1544
|
+
const gate = manager ? createTrustGate({ manager, interactive }) : createDenyAllGate(t("boot.trust_unavailable"));
|
|
1294
1545
|
setTrustGate(gate);
|
|
1295
1546
|
const trust = gate.decide(root);
|
|
1296
1547
|
if (trust.trusted) diags.ok("Trust", trust.reason);
|
|
@@ -1299,11 +1550,11 @@ function buildTrust(config, workDir, diags, interactive) {
|
|
|
1299
1550
|
}
|
|
1300
1551
|
function buildImportGate(config, diags) {
|
|
1301
1552
|
if (getImportGate()) {
|
|
1302
|
-
diags.ok("Instructions", "
|
|
1553
|
+
diags.ok("Instructions", t("boot.import_host_gate"));
|
|
1303
1554
|
return;
|
|
1304
1555
|
}
|
|
1305
1556
|
if (config.trust?.enabled === false) {
|
|
1306
|
-
setImportGate(createAllowAllImportGate("
|
|
1557
|
+
setImportGate(createAllowAllImportGate(t("boot.import_trust_disabled")));
|
|
1307
1558
|
return;
|
|
1308
1559
|
}
|
|
1309
1560
|
const store = safeInit(
|
|
@@ -1312,7 +1563,7 @@ function buildImportGate(config, diags) {
|
|
|
1312
1563
|
diags
|
|
1313
1564
|
);
|
|
1314
1565
|
if (!store) {
|
|
1315
|
-
setImportGate(createDenyAllImportGate("
|
|
1566
|
+
setImportGate(createDenyAllImportGate(t("boot.import_store_unavailable")));
|
|
1316
1567
|
return;
|
|
1317
1568
|
}
|
|
1318
1569
|
for (const err of store.loadErrors) diags.warn("Instructions", err, "import-trust-load-error");
|
|
@@ -1359,7 +1610,7 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1359
1610
|
for (const p of projectContext.skippedInstructions) {
|
|
1360
1611
|
diags.warn(
|
|
1361
1612
|
"Instructions",
|
|
1362
|
-
|
|
1613
|
+
t("boot.instructions_untrusted", { path: p }),
|
|
1363
1614
|
"workspace-untrusted"
|
|
1364
1615
|
);
|
|
1365
1616
|
}
|
|
@@ -1369,7 +1620,7 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1369
1620
|
diags.ok("Provider");
|
|
1370
1621
|
} catch (err) {
|
|
1371
1622
|
if (err instanceof ProviderError && err.code === PROVIDER_NO_CREDENTIAL) {
|
|
1372
|
-
diags.skipped("Provider",
|
|
1623
|
+
diags.skipped("Provider", t("boot.provider_no_credential", { message: err.message }));
|
|
1373
1624
|
} else {
|
|
1374
1625
|
diags.failed(
|
|
1375
1626
|
"Provider",
|
|
@@ -1406,19 +1657,20 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1406
1657
|
}
|
|
1407
1658
|
const skillDirs = resolveSkillDirs({
|
|
1408
1659
|
userDir: skillsDir(config.homeDir),
|
|
1409
|
-
// 插件技能排在最前(优先级最低)且全部带前缀,见 core 的 skill/sources.ts
|
|
1410
1660
|
pluginDirs: plugins.skillDirs,
|
|
1411
|
-
// 宿主注入的紧跟在插件之后(方案 44 §2.1),同样全部带前缀、同样不过工作区
|
|
1412
|
-
// 闸门 —— 它的信任来自「宿主就是跑我们这个库的那个进程」,和 secretStore 同一档
|
|
1413
1661
|
...opts.hostSkillDirs ? { hostDirs: opts.hostSkillDirs } : {},
|
|
1414
1662
|
projectRoot: projectContext.rootDir,
|
|
1415
1663
|
trusted: trust.trusted,
|
|
1416
1664
|
diags
|
|
1417
1665
|
});
|
|
1418
|
-
const skill = safeInit(
|
|
1666
|
+
const skill = safeInit(
|
|
1667
|
+
"Skill",
|
|
1668
|
+
() => new SkillSystem(skillDirs.dirs, { maxIndexed: config.skills?.maxIndexed }),
|
|
1669
|
+
diags
|
|
1670
|
+
);
|
|
1419
1671
|
if (skill) {
|
|
1420
1672
|
for (const detail of issueDetails(skill.getIssues())) {
|
|
1421
|
-
diags.warn("Skill",
|
|
1673
|
+
diags.warn("Skill", t("boot.skill_issue", { detail }), "invalid-skill-file");
|
|
1422
1674
|
}
|
|
1423
1675
|
}
|
|
1424
1676
|
let skillLearner;
|
|
@@ -1442,15 +1694,17 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1442
1694
|
() => new HookManager(opts.logger ? { log: opts.logger } : {}),
|
|
1443
1695
|
diags
|
|
1444
1696
|
);
|
|
1697
|
+
const hookStatus = { sources: [], projectSkipped: false };
|
|
1445
1698
|
if (hookManager && managedPolicy.allowManagedHooksOnly) {
|
|
1446
1699
|
diags.warn(
|
|
1447
1700
|
"Hook",
|
|
1448
|
-
|
|
1701
|
+
t("boot.managed_hooks_only", { path: managedPolicy.path }),
|
|
1449
1702
|
"managed-hooks-only"
|
|
1450
1703
|
);
|
|
1451
1704
|
} else if (hookManager) {
|
|
1452
1705
|
const hooks = loadHookSources({
|
|
1453
1706
|
userPath: hooksPath(config.homeDir),
|
|
1707
|
+
claudeUserPath: claudeUserSettingsPath(),
|
|
1454
1708
|
pluginPaths: plugins.hookPaths,
|
|
1455
1709
|
...projectContext.rootDir ? { projectRoot: projectContext.rootDir } : {},
|
|
1456
1710
|
trusted: trust.trusted,
|
|
@@ -1458,6 +1712,8 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1458
1712
|
diags
|
|
1459
1713
|
});
|
|
1460
1714
|
hookManager.loadEntries(hooks.entries);
|
|
1715
|
+
hookStatus.sources = [...hooks.sources];
|
|
1716
|
+
hookStatus.projectSkipped = hooks.projectSkipped;
|
|
1461
1717
|
}
|
|
1462
1718
|
const cache = new ApprovalCache(config.homeDir);
|
|
1463
1719
|
const permission = safeInit("Perm", () => new PermissionManager(permLevel, cache), diags);
|
|
@@ -1484,7 +1740,7 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1484
1740
|
policyStatus.projectSkipped = policyDirs.projectSkipped;
|
|
1485
1741
|
policyStatus.skippedDir = policyDirs.skippedDir ?? null;
|
|
1486
1742
|
} catch {
|
|
1487
|
-
diags.warn("Policy", "
|
|
1743
|
+
diags.warn("Policy", t("boot.policy_load_failed"), "policy-load-failed");
|
|
1488
1744
|
}
|
|
1489
1745
|
const lists = config.permissions;
|
|
1490
1746
|
const counts = {
|
|
@@ -1497,7 +1753,12 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1497
1753
|
permission.setPermissionRules(lists);
|
|
1498
1754
|
diags.ok(
|
|
1499
1755
|
"Perm",
|
|
1500
|
-
|
|
1756
|
+
t("boot.perm_rules", {
|
|
1757
|
+
total: ruleCount,
|
|
1758
|
+
allow: counts.allow,
|
|
1759
|
+
ask: counts.ask,
|
|
1760
|
+
deny: counts.deny
|
|
1761
|
+
})
|
|
1501
1762
|
);
|
|
1502
1763
|
for (const finding of checkShadowing(compileRuleLists(lists))) {
|
|
1503
1764
|
diags.warn("Perm", finding.message, `rule-${finding.kind}`);
|
|
@@ -1510,10 +1771,6 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1510
1771
|
contextEngine = safeInit(
|
|
1511
1772
|
"Context",
|
|
1512
1773
|
() => new ContextCompressor({
|
|
1513
|
-
// 2026-08-16 合并时收的一笔(compressor.ts 文件头那张 📮 的第一条):
|
|
1514
|
-
// `compression.enabled` / `compression.threshold` 两个键的装配点在这儿,
|
|
1515
|
-
// 而写它们的那一轮禁区正是 `runtime/**`。不接这一行的后果不是少一个功能,
|
|
1516
|
-
// 是**一句谎话** —— 配得进、设置页印得出来、还标着「② 用户级」,而压缩照压。
|
|
1517
1774
|
...compressionOptions(config),
|
|
1518
1775
|
summarize: async (prompt) => {
|
|
1519
1776
|
const utility = provider.getUtilityInfo().selection;
|
|
@@ -1534,8 +1791,6 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1534
1791
|
const capability = resolveModelCapability(config, diags);
|
|
1535
1792
|
return {
|
|
1536
1793
|
config,
|
|
1537
|
-
// 合并**之前**那一份,连同「哪个键是谁写的」那本账 —— 两样都只给设置页的
|
|
1538
|
-
// 来源标注用(判据在 `Services.baseConfig` 上)
|
|
1539
1794
|
baseConfig,
|
|
1540
1795
|
configOrigins: opts.configOrigins ?? getConfigOrigins(),
|
|
1541
1796
|
provider,
|
|
@@ -1559,6 +1814,7 @@ function buildServices(baseConfig, diags, workDir = process.cwd(), opts = {}) {
|
|
|
1559
1814
|
settingsLayers: merged.layers,
|
|
1560
1815
|
managedPolicy,
|
|
1561
1816
|
policy: policyStatus,
|
|
1817
|
+
hook: hookStatus,
|
|
1562
1818
|
plugins,
|
|
1563
1819
|
cleanups
|
|
1564
1820
|
};
|
|
@@ -1567,7 +1823,6 @@ var init_services = __esm({
|
|
|
1567
1823
|
"src/services.ts"() {
|
|
1568
1824
|
}
|
|
1569
1825
|
});
|
|
1570
|
-
|
|
1571
1826
|
// src/turn-steps.ts
|
|
1572
1827
|
function draft() {
|
|
1573
1828
|
return { text: "", reasoning: "", toolCalls: [], toolResults: [] };
|
|
@@ -1586,12 +1841,6 @@ var TurnStepCollector;
|
|
|
1586
1841
|
var init_turn_steps = __esm({
|
|
1587
1842
|
"src/turn-steps.ts"() {
|
|
1588
1843
|
TurnStepCollector = class {
|
|
1589
|
-
/**
|
|
1590
|
-
* @param startedAt 这一轮开跑的毫秒时间戳,用来算 `durationMs`。
|
|
1591
|
-
* @param now 取当下时间。**注进来只为让用例能钉一个确定的耗时** ——
|
|
1592
|
-
* 生产路径上就是 `Date.now()`,而这一层是服务端,用本机钟是对的
|
|
1593
|
-
* (web 那边不许用 `Date.now()`,因为它得用服务端信封上的 `ts`)。
|
|
1594
|
-
*/
|
|
1595
1844
|
constructor(startedAt, now = Date.now) {
|
|
1596
1845
|
this.startedAt = startedAt;
|
|
1597
1846
|
this.now = now;
|
|
@@ -1600,11 +1849,8 @@ var init_turn_steps = __esm({
|
|
|
1600
1849
|
now;
|
|
1601
1850
|
steps = [];
|
|
1602
1851
|
current = null;
|
|
1603
|
-
/** 当前这一步已经收口,下一笔内容另开一步。见文件头那条 ⚠️ */
|
|
1604
1852
|
sealed = false;
|
|
1605
|
-
/** 这一轮发过几次工具调用。**跨步累计**,`toolCalls` 那一格要的是整轮的数 */
|
|
1606
1853
|
calls = 0;
|
|
1607
|
-
/** `finish` 那一帧带回来的账。没收到那一帧就没有 —— 中止 / 报错时就是这样 */
|
|
1608
1854
|
finish = null;
|
|
1609
1855
|
observe(event) {
|
|
1610
1856
|
switch (event.type) {
|
|
@@ -1627,8 +1873,6 @@ var init_turn_steps = __esm({
|
|
|
1627
1873
|
toolCallId: event.callId,
|
|
1628
1874
|
toolName: event.toolName,
|
|
1629
1875
|
output: event.output,
|
|
1630
|
-
// artifact 原样带走(含 `data`)。剥 base64 是落盘那一层的事
|
|
1631
|
-
// (`persistToolResults()`)—— 内存里的宿主要那份数据渲染缩略图
|
|
1632
1876
|
...event.artifacts ? { artifacts: event.artifacts } : {}
|
|
1633
1877
|
});
|
|
1634
1878
|
return;
|
|
@@ -1646,27 +1890,10 @@ var init_turn_steps = __esm({
|
|
|
1646
1890
|
return;
|
|
1647
1891
|
}
|
|
1648
1892
|
}
|
|
1649
|
-
/** 冲掉最后一步,交出整轮。空步已经在 {@link seal} 里滤掉 */
|
|
1650
1893
|
take() {
|
|
1651
1894
|
this.flush();
|
|
1652
1895
|
return this.steps;
|
|
1653
1896
|
}
|
|
1654
|
-
/**
|
|
1655
|
-
* 这一轮的账(决定 8 第三个时刻),落在最后一条 assistant 行上。
|
|
1656
|
-
*
|
|
1657
|
-
* ## 三格各自可以没有,而且**都不补 0**
|
|
1658
|
-
*
|
|
1659
|
-
* - `usage` 没有 = provider 这一轮没上报用量。写 0 会读成「这一轮没花钱」,
|
|
1660
|
-
* 而真相是「不知道」(同 `SessionDB.recordUsage` 那条「宁可少记」)。
|
|
1661
|
-
* - `model` 没有 = 引擎没告诉我们这一轮跑的是谁。**不许拿配置里那个顶上** ——
|
|
1662
|
-
* 降级和 `switch_model` 之后那是个从没跑过的模型名,比不显示更糟。
|
|
1663
|
-
* - `durationMs` 没有 = 起点未知(没给 `startedAt`)。写 0 是「0s」,那是假事实。
|
|
1664
|
-
*
|
|
1665
|
-
* `toolCalls` 反过来 —— 它**必有**,因为 `0` 在这一格是真的 0(「这一轮没动手」)。
|
|
1666
|
-
*
|
|
1667
|
-
* 整帧 `finish` 都没收到(中止 / 报错)时返回 `undefined`:那一轮**没有**账,
|
|
1668
|
-
* 而不是一笔全是空格的账。
|
|
1669
|
-
*/
|
|
1670
1897
|
facts() {
|
|
1671
1898
|
if (!this.finish) return void 0;
|
|
1672
1899
|
const durationMs = this.startedAt === void 0 ? void 0 : Math.max(0, this.finish.at - this.startedAt);
|
|
@@ -1700,46 +1927,20 @@ function settleAbandoned(pending) {
|
|
|
1700
1927
|
var AgentSession;
|
|
1701
1928
|
var init_agent_session = __esm({
|
|
1702
1929
|
"src/agent-session.ts"() {
|
|
1930
|
+
init_events();
|
|
1703
1931
|
init_turn_steps();
|
|
1704
1932
|
AgentSession = class {
|
|
1705
|
-
/**
|
|
1706
|
-
* 当前会话 id。
|
|
1707
|
-
*
|
|
1708
|
-
* **不再是 `readonly`**(方案 25 PR-4 的 `/resume`):换会话时它必须跟着换,
|
|
1709
|
-
* 否则接下来 `store.record()` 会把新对话记到**旧会话**名下 ——
|
|
1710
|
-
* 症状是「我明明 resume 了另一段,怎么两段混在一起」。
|
|
1711
|
-
* 换的入口只有 `resume()` 一个,不要在别处赋值。
|
|
1712
|
-
*/
|
|
1713
1933
|
sessionId;
|
|
1714
1934
|
agent;
|
|
1715
1935
|
store;
|
|
1716
1936
|
checkpoints;
|
|
1717
1937
|
goals;
|
|
1718
1938
|
currentRole;
|
|
1719
|
-
/**
|
|
1720
|
-
* 上一轮是谁在跑(方案 57 §3.5)。`null` = 这段会话此刻没有角色,**那也是一个值**
|
|
1721
|
-
* —— 从「有」变成「没有」(用户点掉 chip 退回一段本来就没有角色的会话)同样
|
|
1722
|
-
* 要留痕。
|
|
1723
|
-
*
|
|
1724
|
-
* ⚠️ **它只在进程内有效,从历史里恢复不出来。** 构造时拿的是这个会话**默认**
|
|
1725
|
-
* 那个专家,于是重启之后第一条不带 chip 的消息**不插标记** —— 那是对的:
|
|
1726
|
-
* 上一条标记还在历史里,语义没断。
|
|
1727
|
-
*
|
|
1728
|
-
* 别去做一次「从历史里反推上一轮是谁」的解析:那要 parse 我们自己印的那句
|
|
1729
|
-
* 中文,而它是可翻译的(catalog 真源 `locales/*.yaml`)—— 换一次界面语言,
|
|
1730
|
-
* 那个解析器就静默失灵。
|
|
1731
|
-
*/
|
|
1732
1939
|
lastRole;
|
|
1733
|
-
|
|
1940
|
+
currentWorkDir;
|
|
1941
|
+
lastWorkDir = null;
|
|
1734
1942
|
history;
|
|
1735
|
-
/** 待交给宿主的请求队列(审批 + 提问)+ 「有新请求」的唤醒信号 */
|
|
1736
1943
|
queue = [];
|
|
1737
|
-
/**
|
|
1738
|
-
* 已经交给宿主、但还没答复的请求。
|
|
1739
|
-
*
|
|
1740
|
-
* 必须单独记:请求一旦 yield 给宿主就已经出了 queue,如果宿主提前 break
|
|
1741
|
-
* 走了,只看 queue 是空的,那个挂在 await 上的引擎就永久等下去。
|
|
1742
|
-
*/
|
|
1743
1944
|
inFlight = /* @__PURE__ */ new Set();
|
|
1744
1945
|
notify = null;
|
|
1745
1946
|
constructor(opts) {
|
|
@@ -1749,15 +1950,10 @@ var init_agent_session = __esm({
|
|
|
1749
1950
|
this.checkpoints = opts.checkpoints;
|
|
1750
1951
|
this.goals = opts.goals;
|
|
1751
1952
|
this.currentRole = opts.currentRole;
|
|
1953
|
+
this.currentWorkDir = opts.currentWorkDir;
|
|
1752
1954
|
this.lastRole = opts.currentRole?.() ?? null;
|
|
1753
1955
|
this.history = [...opts.history ?? []];
|
|
1754
1956
|
}
|
|
1755
|
-
/**
|
|
1756
|
-
* 传给 AgentLoop 的审批回调。
|
|
1757
|
-
*
|
|
1758
|
-
* 必须在构造 AgentLoop **之前**拿到(它是构造参数),所以这里做成静态工厂:
|
|
1759
|
-
* 先建 pending 队列,再建 AgentLoop,最后建 AgentSession。
|
|
1760
|
-
*/
|
|
1761
1957
|
static createApprovalBridge() {
|
|
1762
1958
|
let target = null;
|
|
1763
1959
|
const buffered = [];
|
|
@@ -1783,49 +1979,24 @@ var init_agent_session = __esm({
|
|
|
1783
1979
|
}
|
|
1784
1980
|
};
|
|
1785
1981
|
}
|
|
1786
|
-
/** 当前累积的历史(只读快照) */
|
|
1787
1982
|
getHistory() {
|
|
1788
1983
|
return this.history;
|
|
1789
1984
|
}
|
|
1790
|
-
/**
|
|
1791
|
-
* 往上下文里记一句话,**不跑模型**(方案 25 §2.3 的 `!` 走这条)。
|
|
1792
|
-
*
|
|
1793
|
-
* 用户在 TUI 里敲 `!git status` 时,输出得让模型看得见 —— 「你看一下这个命令的
|
|
1794
|
-
* 输出」正是 `!` 存在的意义。但它不该触发一轮对话:用户只是想让它知道,
|
|
1795
|
-
* 不是想让它现在就说点什么。
|
|
1796
|
-
*
|
|
1797
|
-
* 角色是 `user` 而不是 `system`:这**确实是用户做的事**,把它标成系统消息会让
|
|
1798
|
-
* 模型以为是我们的指令。也**不落盘** —— `run()` 那条路才管持久化,
|
|
1799
|
-
* 而这里没有一轮可以归属;resume 之后丢掉一条「我刚跑了 ls」是可以接受的,
|
|
1800
|
-
* 把它塞进 DB 反而会让会话回放里出现一条没有回答的用户消息。
|
|
1801
|
-
*/
|
|
1802
1985
|
note(text) {
|
|
1803
1986
|
if (!text.trim()) return;
|
|
1804
1987
|
this.history.push({ role: "user", content: text });
|
|
1805
1988
|
}
|
|
1806
|
-
/**
|
|
1807
|
-
* 手动压缩一次上下文(`/compact`,方案 25 §2.6)。
|
|
1808
|
-
*
|
|
1809
|
-
* 和自动压缩共用引擎里同一段实现,区别只有两个:**不看阈值**(用户敲这条命令
|
|
1810
|
-
* 就是要它现在压),以及可以带一句「这次帮我留住什么」。
|
|
1811
|
-
*
|
|
1812
|
-
* 压缩本身是一次真实的 LLM 请求,那笔钱进的是同一个会话预算 —— 账单由
|
|
1813
|
-
* `AgentLoop` 抛出来,这里原样转给调用方,别在这一层吞掉。
|
|
1814
|
-
*
|
|
1815
|
-
* 历史只有一条(或空)时直接不压:压一条消息拿不到任何收益,
|
|
1816
|
-
* 却要花一次 utility 模型的请求。
|
|
1817
|
-
*/
|
|
1818
1989
|
async compact(instruction) {
|
|
1819
1990
|
const before = this.history.length;
|
|
1820
1991
|
if (before <= 1) {
|
|
1821
|
-
return { ran: false, before, after: before, reason: "
|
|
1992
|
+
return { ran: false, before, after: before, reason: t("boot.compact_too_short") };
|
|
1822
1993
|
}
|
|
1823
1994
|
const result = await this.agent.compactNow(
|
|
1824
1995
|
this.history.map((m) => ({ role: m.role, content: m.content })),
|
|
1825
1996
|
instruction
|
|
1826
1997
|
);
|
|
1827
1998
|
if (!result.ran) {
|
|
1828
|
-
return { ran: false, before, after: before, reason: "
|
|
1999
|
+
return { ran: false, before, after: before, reason: t("boot.compact_unavailable") };
|
|
1829
2000
|
}
|
|
1830
2001
|
this.history = result.messages.map((m) => ({
|
|
1831
2002
|
role: m.role === "assistant" ? "assistant" : "user",
|
|
@@ -1838,23 +2009,6 @@ var init_agent_session = __esm({
|
|
|
1838
2009
|
...result.usage ? { usage: result.usage } : {}
|
|
1839
2010
|
};
|
|
1840
2011
|
}
|
|
1841
|
-
/**
|
|
1842
|
-
* 把某个会话落盘的那段历史装进内存 —— **「模型看到的」那一份只有这一个装法**。
|
|
1843
|
-
*
|
|
1844
|
-
* 三处共用:{@link resume}(换到另一段对话)、{@link rewindConversation}
|
|
1845
|
-
* (回退之后重装),以及 web 的复活(`SessionRuntimeFactory.revive()`:
|
|
1846
|
-
* 引擎装好之后把落盘的那段接上)。抄成三份的代价不是「重复」,
|
|
1847
|
-
* 而是**它们一旦分叉,症状就是「屏幕上和模型看到的对不上」** ——
|
|
1848
|
-
* 下面 {@link resume} 那段注释骂的正是这个。
|
|
1849
|
-
*
|
|
1850
|
-
* 没有 store(纯内存模式)就装成空的:没有 DB 就没有历史可言。
|
|
1851
|
-
*
|
|
1852
|
-
* ⚠️ **不改 `this.sessionId`。** 换会话是 {@link resume} 的事 ——
|
|
1853
|
-
* 复活装的就是自己这一段,那儿本来就不该动。
|
|
1854
|
-
*
|
|
1855
|
-
* @param sessionId 从哪个会话读
|
|
1856
|
-
* @returns 装进去的那一份,供宿主回放进屏幕
|
|
1857
|
-
*/
|
|
1858
2012
|
reloadHistory(sessionId) {
|
|
1859
2013
|
const restored = this.store?.loadHistory(sessionId) ?? [];
|
|
1860
2014
|
this.history = restored.map((m) => ({
|
|
@@ -1864,38 +2018,11 @@ var init_agent_session = __esm({
|
|
|
1864
2018
|
}));
|
|
1865
2019
|
return this.history;
|
|
1866
2020
|
}
|
|
1867
|
-
/**
|
|
1868
|
-
* 换到另一个会话(`/resume`,方案 25 §2.6)。
|
|
1869
|
-
*
|
|
1870
|
-
* **模型真的接上那段上下文**,不只是把历史打印到屏幕上 —— 后者在终端里
|
|
1871
|
-
* 和真恢复长得一模一样,而用户会照着屏幕上那段去问「你刚才说的那个方案」,
|
|
1872
|
-
* 然后收到一句完全对不上的回答。
|
|
1873
|
-
*
|
|
1874
|
-
* 两样都要换:历史(模型看的)和 `sessionId`(往哪儿记)。只换前者的话,
|
|
1875
|
-
* 接下来这一轮会被记进**上一个**会话里 —— 所以这儿在
|
|
1876
|
-
* {@link reloadHistory} 之外还多一步。
|
|
1877
|
-
*
|
|
1878
|
-
* 内存模式下装出来是空的,但 `sessionId` 仍然换:调用方已经决定
|
|
1879
|
-
* 「这是另一段对话」了。
|
|
1880
|
-
*
|
|
1881
|
-
* @returns 恢复出来的历史,供宿主回放进屏幕
|
|
1882
|
-
*/
|
|
1883
2021
|
resume(sessionId) {
|
|
1884
2022
|
const restored = this.reloadHistory(sessionId);
|
|
1885
2023
|
this.sessionId = sessionId;
|
|
1886
2024
|
return restored;
|
|
1887
2025
|
}
|
|
1888
|
-
/**
|
|
1889
|
-
* 对话回退(方案 27):删掉水位线之后的消息,然后把内存历史重新装一遍。
|
|
1890
|
-
*
|
|
1891
|
-
* **重装而不是自己在内存里 splice**:DB 是唯一真源,重装出来的历史天然和它一致。
|
|
1892
|
-
* 自己截内存的话就有两套截断逻辑,而它们一旦分叉,症状是「屏幕上和模型看到的
|
|
1893
|
-
* 对不上」—— `resume()` 那段注释骂过的正是这个。
|
|
1894
|
-
*
|
|
1895
|
-
* 没有 store(纯内存模式)就什么都不做并返回 `null`:没有 DB 就没有水位线可言。
|
|
1896
|
-
*
|
|
1897
|
-
* @returns 删掉几条消息;失败或不支持时 `null`
|
|
1898
|
-
*/
|
|
1899
2026
|
rewindConversation(lastKeptId) {
|
|
1900
2027
|
if (!this.store || lastKeptId < 0) return null;
|
|
1901
2028
|
const removed = this.store.rewindTo(this.sessionId, lastKeptId);
|
|
@@ -1903,13 +2030,6 @@ var init_agent_session = __esm({
|
|
|
1903
2030
|
this.reloadHistory(this.sessionId);
|
|
1904
2031
|
return removed;
|
|
1905
2032
|
}
|
|
1906
|
-
/**
|
|
1907
|
-
* 一条排队的请求 → 一个带答复闭包的事件。
|
|
1908
|
-
*
|
|
1909
|
-
* **答复一次就出账**:先从 `inFlight` 里删掉,再 resolve。重复调用 respond
|
|
1910
|
-
* 是无害的(promise 已 settle),但漏了这一步 `finally` 会再对它补一个兜底答复,
|
|
1911
|
-
* 于是「用户明明点了允许」和「引擎收到拒绝」同时成立。
|
|
1912
|
-
*/
|
|
1913
2033
|
toEvent(pending) {
|
|
1914
2034
|
if (pending.kind === "question") {
|
|
1915
2035
|
return {
|
|
@@ -1924,26 +2044,12 @@ var init_agent_session = __esm({
|
|
|
1924
2044
|
return {
|
|
1925
2045
|
type: "approval-request",
|
|
1926
2046
|
request: pending.req,
|
|
1927
|
-
// 没带 `note` 时原样传裸出口而不是包成 `{outcome}`:两者语义相同,
|
|
1928
|
-
// 但裸串是既有形态,包一层会让所有断言 `resolve` 参数的用例白红一次
|
|
1929
2047
|
respond: (outcome, note) => {
|
|
1930
2048
|
this.inFlight.delete(pending);
|
|
1931
2049
|
pending.resolve(note === void 0 ? outcome : { outcome, note });
|
|
1932
2050
|
}
|
|
1933
2051
|
};
|
|
1934
2052
|
}
|
|
1935
|
-
/**
|
|
1936
|
-
* 跑一轮对话,产出 AgentEvent 流(含 approval-request / question)。
|
|
1937
|
-
*
|
|
1938
|
-
* 宿主只要 for-await 这个流,把 approval-request 交给用户点选即可 ——
|
|
1939
|
-
* 不用关心阻塞回调、不用自己拼历史、不用自己写持久化。
|
|
1940
|
-
*
|
|
1941
|
-
* @param opts.signal 中止信号。`AgentLoop` 一直支持 `input.signal`(轮次之间
|
|
1942
|
-
* 和工具执行处都会检查),但这个签名以前不透传,于是宿主唯一的中止手段
|
|
1943
|
-
* 是硬杀进程 —— `dispose()` 跑不到,SQLite 连接和子进程都留着。
|
|
1944
|
-
* TUI 的 Esc 就靠它:只靠 `break` 退出 for-await 要等下一个事件到达
|
|
1945
|
-
* 才生效,模型正在长考时按 Esc 完全没反应。
|
|
1946
|
-
*/
|
|
1947
2053
|
async *run(userMessage, opts = {}) {
|
|
1948
2054
|
let assistantText = "";
|
|
1949
2055
|
const userText = contentToText(userMessage);
|
|
@@ -1955,13 +2061,11 @@ var init_agent_session = __esm({
|
|
|
1955
2061
|
});
|
|
1956
2062
|
if (userText.trim()) this.goals?.beginRound(this.sessionId);
|
|
1957
2063
|
this.markRoleHandoff();
|
|
2064
|
+
this.markWorkspaceHandoff();
|
|
1958
2065
|
const iter = this.agent.stream({
|
|
1959
2066
|
userMessage,
|
|
1960
2067
|
sessionId: this.sessionId,
|
|
1961
2068
|
history: this.history,
|
|
1962
|
-
// 逐轮把真实用量累进 `sessions` 表(方案 42 PR-0)。
|
|
1963
|
-
// 收的是**本轮增量** —— 事件流里的 `usage.cumulative` 会被 BudgetStore
|
|
1964
|
-
// 从上次会话恢复,减不出干净的增量(见 `RunInput.onTurnUsage`)
|
|
1965
2069
|
...this.store ? {
|
|
1966
2070
|
onTurnUsage: (usage) => void this.store?.recordUsage(this.sessionId, usage)
|
|
1967
2071
|
} : {},
|
|
@@ -1980,7 +2084,9 @@ var init_agent_session = __esm({
|
|
|
1980
2084
|
while (this.queue.length > 0) {
|
|
1981
2085
|
const pending = this.queue.shift();
|
|
1982
2086
|
this.inFlight.add(pending);
|
|
1983
|
-
|
|
2087
|
+
const request = this.toEvent(pending);
|
|
2088
|
+
publishAgentEvent(this.sessionId, request);
|
|
2089
|
+
yield request;
|
|
1984
2090
|
}
|
|
1985
2091
|
continue;
|
|
1986
2092
|
}
|
|
@@ -1989,6 +2095,7 @@ var init_agent_session = __esm({
|
|
|
1989
2095
|
pendingNext = iter.next();
|
|
1990
2096
|
if (event.type === "text-delta") assistantText += event.text;
|
|
1991
2097
|
turn.observe(event);
|
|
2098
|
+
publishAgentEvent(this.sessionId, event);
|
|
1992
2099
|
yield event;
|
|
1993
2100
|
}
|
|
1994
2101
|
} finally {
|
|
@@ -1997,47 +2104,20 @@ var init_agent_session = __esm({
|
|
|
1997
2104
|
this.inFlight.clear();
|
|
1998
2105
|
this.notify = null;
|
|
1999
2106
|
}
|
|
2000
|
-
this.history.push({ role: "user", content: userText, ...parts ? { parts } : {} });
|
|
2001
|
-
if (assistantText) this.history.push({ role: "assistant", content: assistantText });
|
|
2002
2107
|
const steps = turn.take();
|
|
2003
2108
|
const facts = turn.facts();
|
|
2109
|
+
const assistantHistory = assistantTurnText(
|
|
2110
|
+
assistantText,
|
|
2111
|
+
steps.flatMap((s) => s.toolResults ?? [])
|
|
2112
|
+
);
|
|
2113
|
+
this.history.push({ role: "user", content: userText, ...parts ? { parts } : {} });
|
|
2114
|
+
if (assistantHistory) this.history.push({ role: "assistant", content: assistantHistory });
|
|
2004
2115
|
this.store?.record(this.sessionId, userText, assistantText, {
|
|
2005
2116
|
...parts ? { parts } : {},
|
|
2006
2117
|
...steps.length > 0 ? { steps } : {},
|
|
2007
|
-
// 这一轮的账(决定 8 第三个时刻)。不落它的话刷新之后每一轮的
|
|
2008
|
-
// 「共 N token · 哪个模型 · 花了几秒」全部消失 —— 那是网线补不回来的
|
|
2009
2118
|
...facts ? { facts } : {}
|
|
2010
2119
|
});
|
|
2011
2120
|
}
|
|
2012
|
-
/**
|
|
2013
|
-
* 换专家的那一刻,往历史里插一条 `[系统]` 消息(方案 57 §2.3 / §3.5)。
|
|
2014
|
-
*
|
|
2015
|
-
* ## 为什么在这一层,不在 `loop.ts` 里
|
|
2016
|
-
*
|
|
2017
|
-
* 循环里那个 `messages` 是 **per-run** 的数组,往那儿 push 的东西不进 DB 历史
|
|
2018
|
-
* (后台任务通知就是这么设计的 —— 它不该被回放)。而这一条要的**恰恰是落盘**:
|
|
2019
|
-
* 身份行活在那一次请求里、chip 活在浏览器里,历史是唯一会落盘的那一份,
|
|
2020
|
-
* 而「这段对话里谁干的」正是重开会话回放时才会被问的问题。
|
|
2021
|
-
*
|
|
2022
|
-
* ## 三条形状判据
|
|
2023
|
-
*
|
|
2024
|
-
* 1. **`role: 'user'` + `[系统]` 前缀,不是 `role: 'system'`** —— AI SDK v7 不许
|
|
2025
|
-
* `messages` 里出现 system,会抛 `AI_InvalidPromptError`。这条坑 `loop.ts`
|
|
2026
|
-
* 已经踩过一次并逐字记下来了,别重踩。⚠️ 前缀**由这里拼**({@link systemNote}),
|
|
2027
|
-
* catalog 里 `agent_role` 那两条是光正文:前缀是全仓共用的一条契约,真源在
|
|
2028
|
-
* `protocol/src/system-note.ts`,烤进句子里就会跟着译者走;
|
|
2029
|
-
* 2. **两条连续的 `user` 消息是安全的** —— 后台任务通知今天就是这么挂的,
|
|
2030
|
-
* 而它在真 provider 上跑通了。⚠️ `message-utils.ts` 里那个
|
|
2031
|
-
* `validateAlternation()` 看起来禁止这件事,但它**全仓只有自己的用例调用,
|
|
2032
|
-
* 生产路径上一个调用点都没有**(2026-08-17 复核)。别为了让它自洽去「修」
|
|
2033
|
-
* 这条路,那会连后台通知一起打挂;
|
|
2034
|
-
* 3. **只在真的换人那一条上插**(含「从有到无」)。每条都插等于往历史里灌噪音,
|
|
2035
|
-
* 还会让压缩器把预算花在这上面。
|
|
2036
|
-
*
|
|
2037
|
-
* ⚠️ 落盘走 {@link SessionStore.recordNote} 而不是 `record()`:后者写的是**一轮
|
|
2038
|
-
* 问答**(user + assistant + tool×N),拿它写一条孤零零的 user 行是在赌
|
|
2039
|
-
* 「assistantText 为空时它恰好不写第二行」——那是一个实现细节,不是一条承诺。
|
|
2040
|
-
*/
|
|
2041
2121
|
markRoleHandoff() {
|
|
2042
2122
|
if (!this.currentRole) return;
|
|
2043
2123
|
const now = this.currentRole() ?? null;
|
|
@@ -2048,14 +2128,24 @@ var init_agent_session = __esm({
|
|
|
2048
2128
|
this.history.push({ role: "user", content: text });
|
|
2049
2129
|
this.store?.recordNote(this.sessionId, text);
|
|
2050
2130
|
}
|
|
2051
|
-
|
|
2131
|
+
markWorkspaceHandoff() {
|
|
2132
|
+
if (!this.currentWorkDir) return;
|
|
2133
|
+
const now = this.currentWorkDir();
|
|
2134
|
+
const previous = this.lastWorkDir;
|
|
2135
|
+
this.lastWorkDir = now;
|
|
2136
|
+
if (previous === null || previous === now) return;
|
|
2137
|
+
if (this.history.length === 0) return;
|
|
2138
|
+
const text = systemNote(t("workspace.handoff", { root: now }));
|
|
2139
|
+
this.history.push({ role: "user", content: text });
|
|
2140
|
+
this.store?.recordNote(this.sessionId, text);
|
|
2141
|
+
}
|
|
2052
2142
|
async once(userMessage, opts = {}) {
|
|
2053
2143
|
let text = "";
|
|
2054
2144
|
let reasoning = "";
|
|
2055
2145
|
for await (const ev of this.run(userMessage, opts)) {
|
|
2056
2146
|
if (ev.type === "text-delta") text += ev.text;
|
|
2057
2147
|
else if (ev.type === "reasoning-delta") reasoning += ev.text;
|
|
2058
|
-
else if (ev.type === "error") text =
|
|
2148
|
+
else if (ev.type === "error") text = t("boot.error", { message: ev.message });
|
|
2059
2149
|
}
|
|
2060
2150
|
return { text, ...reasoning ? { reasoning } : {} };
|
|
2061
2151
|
}
|
|
@@ -2065,8 +2155,6 @@ var init_agent_session = __esm({
|
|
|
2065
2155
|
function createRoleControl(scope, roles) {
|
|
2066
2156
|
return {
|
|
2067
2157
|
has: (name) => findRole(roles(), name) !== void 0,
|
|
2068
|
-
// `findRole` 找不到时回 `undefined`,而 `enter(undefined)` 的语义正好是
|
|
2069
|
-
// 「退回会话默认角色」—— 上面那段 ⚠️ 说的就是这一格,不用再判一次
|
|
2070
2158
|
enterTurn: (name) => scope.enter(findRole(roles(), name))
|
|
2071
2159
|
};
|
|
2072
2160
|
}
|
|
@@ -2086,7 +2174,6 @@ var init_role_scope = __esm({
|
|
|
2086
2174
|
"src/role-scope.ts"() {
|
|
2087
2175
|
}
|
|
2088
2176
|
});
|
|
2089
|
-
|
|
2090
2177
|
// src/session-goal.ts
|
|
2091
2178
|
function buildGoalControl(service, sessionId) {
|
|
2092
2179
|
return {
|
|
@@ -2100,20 +2187,47 @@ function buildGoalControl(service, sessionId) {
|
|
|
2100
2187
|
clear: () => service.clear(sessionId())
|
|
2101
2188
|
};
|
|
2102
2189
|
}
|
|
2190
|
+
function buildGoalCatalog(service) {
|
|
2191
|
+
return {
|
|
2192
|
+
current: (sessionId) => service.current(sessionId),
|
|
2193
|
+
create: (sessionId, objective, maxRounds) => service.create(sessionId, objective, maxRounds),
|
|
2194
|
+
edit: (sessionId, objective) => service.edit(sessionId, objective),
|
|
2195
|
+
setBudget: (sessionId, maxRounds) => service.setBudget(sessionId, maxRounds),
|
|
2196
|
+
pause: (sessionId) => service.pause(sessionId),
|
|
2197
|
+
resume: (sessionId) => service.resume(sessionId),
|
|
2198
|
+
complete: (sessionId, evidence) => service.complete(sessionId, evidence),
|
|
2199
|
+
clear: (sessionId) => service.clear(sessionId)
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2103
2202
|
var init_session_goal = __esm({
|
|
2104
2203
|
"src/session-goal.ts"() {
|
|
2105
2204
|
}
|
|
2106
2205
|
});
|
|
2107
2206
|
function buildSessionGuards(input) {
|
|
2108
|
-
const {
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
}
|
|
2207
|
+
const {
|
|
2208
|
+
permission,
|
|
2209
|
+
planState,
|
|
2210
|
+
managed,
|
|
2211
|
+
startLevel,
|
|
2212
|
+
requestApproval,
|
|
2213
|
+
interactive,
|
|
2214
|
+
deferredTools
|
|
2215
|
+
} = input;
|
|
2216
|
+
const bootstrapReveal = input.bootstrapReveal;
|
|
2217
|
+
const scopeOf = (perm, state, own) => {
|
|
2218
|
+
const reveal = own ? createToolRevealState() : bootstrapReveal;
|
|
2219
|
+
return {
|
|
2220
|
+
permission: perm,
|
|
2221
|
+
permissions: perm ? buildSessionPermissions(perm, managed, state) : null,
|
|
2222
|
+
plan: state ? buildPlanControl(state) : null,
|
|
2223
|
+
planState: state,
|
|
2224
|
+
tools: own ? [
|
|
2225
|
+
...state ? createPlanModeTools({ state, ...interactive ? { requestApproval } : {} }) : [],
|
|
2226
|
+
...deferredTools().length > 0 ? [createToolSearchTool({ deferred: deferredTools, reveal })] : []
|
|
2227
|
+
] : [],
|
|
2228
|
+
reveal
|
|
2229
|
+
};
|
|
2230
|
+
};
|
|
2117
2231
|
const bootstrapScope = scopeOf(permission, planState, false);
|
|
2118
2232
|
return {
|
|
2119
2233
|
bootstrap: () => bootstrapScope,
|
|
@@ -2216,19 +2330,6 @@ var init_session_store = __esm({
|
|
|
2216
2330
|
"src/session-store.ts"() {
|
|
2217
2331
|
init_session_file_changes();
|
|
2218
2332
|
SessionStore = class {
|
|
2219
|
-
/**
|
|
2220
|
-
* @param artifactsRoot artifact 目录的**家目录**(即 `~/.epoch` 那一层),
|
|
2221
|
-
* 缺省按 `EPOCH_HOME` / profile 解析。只有测试和多 profile 场景要显式给。
|
|
2222
|
-
* @param cwd 这段会话归属的**项目根**,落进 `sessions.cwd` 列(方案 29 §2.1)。
|
|
2223
|
-
*
|
|
2224
|
-
* `cwd` 在方案 29 之前从来没人写过:列是有的(v1 基线里就在,还带着
|
|
2225
|
-
* `idx_sessions_cwd` 索引),但 `ensure()` 只传了 id,于是全库的 cwd 都是 NULL、
|
|
2226
|
-
* `epoch -c`「接上本目录最近一个会话」压根无从判起。
|
|
2227
|
-
*
|
|
2228
|
-
* 传项目根而不是 `process.cwd()`:在子目录里敲 `epoch -c` 要接上同一个仓库的
|
|
2229
|
-
* 会话。读取侧(`SessionManager.getLatestByCwd`)用的是同一个口径。
|
|
2230
|
-
* 不给就仍然写 NULL —— 那段会话只是不参与 `-c` 匹配,别的一切照旧。
|
|
2231
|
-
*/
|
|
2232
2333
|
constructor(sessions, artifactsRoot, cwd, source, decision) {
|
|
2233
2334
|
this.sessions = sessions;
|
|
2234
2335
|
this.artifactsRoot = artifactsRoot;
|
|
@@ -2241,40 +2342,10 @@ var init_session_store = __esm({
|
|
|
2241
2342
|
cwd;
|
|
2242
2343
|
source;
|
|
2243
2344
|
decision;
|
|
2244
|
-
/** 已经确认存在的会话 id,避免每轮都试着 create */
|
|
2245
2345
|
known = /* @__PURE__ */ new Set();
|
|
2246
|
-
/** 这个会话的 artifact 目录 */
|
|
2247
2346
|
artifactDir(sessionId) {
|
|
2248
2347
|
return this.artifactsRoot ? artifactsDir(sessionId, this.artifactsRoot) : artifactsDir(sessionId);
|
|
2249
2348
|
}
|
|
2250
|
-
/**
|
|
2251
|
-
* 记录一轮问答。持久化失败不该影响对话本身,所以吞异常但返回是否成功。
|
|
2252
|
-
*
|
|
2253
|
-
* `userMessage` / `assistantText` 收的是**文本投影**:`Message.content` 是
|
|
2254
|
-
* SQLite 里的一个 TEXT 列,把 base64 灌进去会让 sessions.db 以每张截图几百 KB
|
|
2255
|
-
* 的速度膨胀,而且 `epoch sessions list` 的预览、标题截取、全文检索全会被那串
|
|
2256
|
-
* 字符污染。图片本身走 `detail.parts` —— 落进 artifact 目录,列里只留路径。
|
|
2257
|
-
*
|
|
2258
|
-
* 落盘的行顺序是 **user →(assistant → tool×k)× 步数**,不是时间序:这是
|
|
2259
|
-
* `EpochMessage` 的规范顺序(assistant 声明 `toolCalls`,tool 行携带
|
|
2260
|
-
* `toolResults`)。宿主按 `toolCallId` 配对分组渲染,**不要依赖行序**去猜
|
|
2261
|
-
* 哪个结果属于哪次调用 —— 那正是 `AgentEvent` 加 `callId` 时骂过的病
|
|
2262
|
-
* (靠「工具名 + 调用顺序 FIFO」猜,并行工具必然串台)。
|
|
2263
|
-
*
|
|
2264
|
-
* ## ⚠️ 一轮不再只有**一条** assistant 行(2026-08-18)
|
|
2265
|
-
*
|
|
2266
|
-
* 以前是「整轮正文拼成一条 + 全部 toolCalls 挂上去 + 所有 tool 行垫在最后」。
|
|
2267
|
-
* 那个形状丢掉了步序,直接后果是**刷新页面之后屏幕上的顺序和刚才不一样**:
|
|
2268
|
-
* 回放只能照行序重建,于是「先调工具、后回答」被画成「先回答、后工具卡」。
|
|
2269
|
-
*
|
|
2270
|
-
* 现在按 {@link TurnDetail.steps} 一步一行。这不是新发明的形状 —— `loop.ts`
|
|
2271
|
-
* 自己往上下文里 push 的就是它,只是那一份是 per-run 的、从不落盘。
|
|
2272
|
-
*
|
|
2273
|
-
* 两个连带的地方,改这里时别忘了:
|
|
2274
|
-
* - {@link loadHistory} 要把连续的 assistant 行**合并回一条**再喂模型;
|
|
2275
|
-
* - `web/src/state/replay.ts` 和假服务端的 `toReplayMessages()` 是读侧的
|
|
2276
|
-
* 两份实现,形状变了两边都要跟。
|
|
2277
|
-
*/
|
|
2278
2349
|
record(sessionId, userMessage, assistantText, detail = {}) {
|
|
2279
2350
|
try {
|
|
2280
2351
|
this.ensure(sessionId);
|
|
@@ -2298,8 +2369,6 @@ var init_session_store = __esm({
|
|
|
2298
2369
|
for (const result of persistToolResults(step.toolResults ?? [])) {
|
|
2299
2370
|
this.sessions.appendMessage(sessionId, {
|
|
2300
2371
|
role: "tool",
|
|
2301
|
-
// content 列存输出的文本投影:FTS 索引和 `epoch sessions search`
|
|
2302
|
-
// 靠它才搜得到工具输出。结构化的那份在 toolResults 里
|
|
2303
2372
|
content: result.output,
|
|
2304
2373
|
toolCallId: result.toolCallId,
|
|
2305
2374
|
toolName: result.toolName,
|
|
@@ -2312,18 +2381,6 @@ var init_session_store = __esm({
|
|
|
2312
2381
|
return false;
|
|
2313
2382
|
}
|
|
2314
2383
|
}
|
|
2315
|
-
/**
|
|
2316
|
-
* 记一条**孤立的 `user` 行**,没有与之配对的回答(方案 57 §3.5 那条换人留痕)。
|
|
2317
|
-
*
|
|
2318
|
-
* 和 {@link record} 分开而不是拿 `record(id, text, '')` 顶:那个方法写的是**一轮
|
|
2319
|
-
* 问答**,「assistantText 为空时它恰好只写一行」是一个实现细节而不是一条承诺 ——
|
|
2320
|
-
* 哪天它改成「assistant 行无条件写」(那是一个合理的改动),这条留痕后面就会
|
|
2321
|
-
* 凭空多出一条空回答,而没有任何东西会红。
|
|
2322
|
-
*
|
|
2323
|
-
* **要 `ensure()`**,判据同 `recordUsage()`:这一行写在轮次开始那一刻,
|
|
2324
|
-
* 比 `record()`(整轮结束才写)早。不建会话行的话 `appendMessage` 会失败,
|
|
2325
|
-
* 而这条留痕正是重开会话回放时唯一说得出「从这里起换人了」的东西。
|
|
2326
|
-
*/
|
|
2327
2384
|
recordNote(sessionId, text) {
|
|
2328
2385
|
try {
|
|
2329
2386
|
this.ensure(sessionId);
|
|
@@ -2333,15 +2390,6 @@ var init_session_store = __esm({
|
|
|
2333
2390
|
return false;
|
|
2334
2391
|
}
|
|
2335
2392
|
}
|
|
2336
|
-
/**
|
|
2337
|
-
* 累加一轮模型请求的真实用量(方案 42 PR-0)。持久化失败照旧不影响对话。
|
|
2338
|
-
*
|
|
2339
|
-
* **这里要 `ensure()`**,和 `lastMessageId()` 的克制正相反:用量是逐轮来的,
|
|
2340
|
-
* 比 `record()`(整轮对话结束才写)早,新会话的第一条消息还没落盘。
|
|
2341
|
-
* 不建会话行的话那条 UPDATE 影响 0 行 —— 一段全新会话的用量会**整段丢掉**。
|
|
2342
|
-
* 而且这里的触发条件比那边硬:能走到这儿说明模型请求已经发出去、钱已经花了,
|
|
2343
|
-
* 那就是一段值得记录的会话,不是「点开又退出」。
|
|
2344
|
-
*/
|
|
2345
2393
|
recordUsage(sessionId, usage) {
|
|
2346
2394
|
try {
|
|
2347
2395
|
this.ensure(sessionId);
|
|
@@ -2351,17 +2399,6 @@ var init_session_store = __esm({
|
|
|
2351
2399
|
return false;
|
|
2352
2400
|
}
|
|
2353
2401
|
}
|
|
2354
|
-
/**
|
|
2355
|
-
* 记下(或清掉)这段会话当前生效的那份已批准计划(方案 35 PR-2 验收 9)。
|
|
2356
|
-
*
|
|
2357
|
-
* **要 `ensure()`**,和 `recordUsage()` 同一个理由、比它更硬:计划被批准
|
|
2358
|
-
* 通常发生在第一轮对话**结束之前**(模型 `enter_plan_mode` → 调研 →
|
|
2359
|
-
* `exit_plan_mode` → 用户批),那时 `record()` 还一条都没写。不建会话行的话
|
|
2360
|
-
* 那条 UPDATE 影响 0 行 —— 用户刚点完「批准并执行」,计划就已经没落盘。
|
|
2361
|
-
*
|
|
2362
|
-
* 传 `null` 是**清掉**(用户拒绝了计划)。这里不做「空串当没给」的归一:
|
|
2363
|
-
* 调用方是 `PlanModeState`,它给的只会是一份正文或者 `null`。
|
|
2364
|
-
*/
|
|
2365
2402
|
savePlan(sessionId, markdown) {
|
|
2366
2403
|
try {
|
|
2367
2404
|
this.ensure(sessionId);
|
|
@@ -2371,12 +2408,6 @@ var init_session_store = __esm({
|
|
|
2371
2408
|
return false;
|
|
2372
2409
|
}
|
|
2373
2410
|
}
|
|
2374
|
-
/**
|
|
2375
|
-
* 读回这段会话的已批准计划;没有(或会话不存在)时 `null`。
|
|
2376
|
-
*
|
|
2377
|
-
* **不 `ensure()`**:这是查询。`--resume` 指着一个不存在的 id 时该回「没有」,
|
|
2378
|
-
* 而不是顺手建一段空会话。
|
|
2379
|
-
*/
|
|
2380
2411
|
loadPlan(sessionId) {
|
|
2381
2412
|
try {
|
|
2382
2413
|
return this.sessions.get(sessionId)?.meta.plan ?? null;
|
|
@@ -2384,13 +2415,6 @@ var init_session_store = __esm({
|
|
|
2384
2415
|
return null;
|
|
2385
2416
|
}
|
|
2386
2417
|
}
|
|
2387
|
-
/**
|
|
2388
|
-
* 这个会话最后一条消息的 DB id(方案 27 的检查点水位线)。
|
|
2389
|
-
*
|
|
2390
|
-
* 会话还没建出来时是 0 —— **不 `ensure()`**:开轮是每条用户消息都要做的事,
|
|
2391
|
-
* 为它去建一个可能马上就没有任何内容的会话行,等于让「只是聊了两句就退出」
|
|
2392
|
-
* 也在 DB 里留一条空会话。
|
|
2393
|
-
*/
|
|
2394
2418
|
lastMessageId(sessionId) {
|
|
2395
2419
|
try {
|
|
2396
2420
|
return this.sessions.lastMessageId(sessionId);
|
|
@@ -2398,13 +2422,6 @@ var init_session_store = __esm({
|
|
|
2398
2422
|
return 0;
|
|
2399
2423
|
}
|
|
2400
2424
|
}
|
|
2401
|
-
/**
|
|
2402
|
-
* 对话回退:删掉水位线之后的消息(方案 27)。返回删了几条,失败时 `null`。
|
|
2403
|
-
*
|
|
2404
|
-
* 吞异常的口径和 `record()` 一致 —— 但这里返回 `null` 而不是 `false`:
|
|
2405
|
-
* 调用方要据此决定还要不要重装内存历史,而「删了 0 条」和「没删成」
|
|
2406
|
-
* 对那个决定是两回事。
|
|
2407
|
-
*/
|
|
2408
2425
|
rewindTo(sessionId, lastKeptId) {
|
|
2409
2426
|
try {
|
|
2410
2427
|
return this.sessions.rewindTo(sessionId, lastKeptId);
|
|
@@ -2412,23 +2429,14 @@ var init_session_store = __esm({
|
|
|
2412
2429
|
return null;
|
|
2413
2430
|
}
|
|
2414
2431
|
}
|
|
2415
|
-
/**
|
|
2416
|
-
* 会话不存在就建。create 对已存在的 id 会抛错,这里按「已存在」处理。
|
|
2417
|
-
*
|
|
2418
|
-
* `cwd` 只在**新建**时写,不去 update 已存在的那一行:一段会话属于哪个项目
|
|
2419
|
-
* 是它开始那一刻的事实,`epoch -c` 之后在别的目录里接着聊也不该把它改籍。
|
|
2420
|
-
*
|
|
2421
|
-
* ⚠️ 工作区决定(v7 那两格)走的是同一条「只在新建时写」,但**理由不同**:
|
|
2422
|
-
* 不是「不该改」,而是「改它的路在别处」—— 那一格的 update 归
|
|
2423
|
-
* `SessionWorkspaces` 的 `decisions.record()`(判据在构造函数的 `decision` 上)。
|
|
2424
|
-
*/
|
|
2425
2432
|
ensure(sessionId) {
|
|
2426
2433
|
if (this.known.has(sessionId)) return;
|
|
2427
2434
|
const decision = this.decision?.(sessionId) ?? null;
|
|
2435
|
+
const cwd = this.cwd?.();
|
|
2428
2436
|
try {
|
|
2429
2437
|
this.sessions.create({
|
|
2430
2438
|
id: sessionId,
|
|
2431
|
-
...
|
|
2439
|
+
...cwd ? { cwd } : {},
|
|
2432
2440
|
...this.source ? { source: this.source } : {},
|
|
2433
2441
|
...decision ? {
|
|
2434
2442
|
workspaceState: decision.state,
|
|
@@ -2439,61 +2447,42 @@ var init_session_store = __esm({
|
|
|
2439
2447
|
}
|
|
2440
2448
|
this.known.add(sessionId);
|
|
2441
2449
|
}
|
|
2442
|
-
/**
|
|
2443
|
-
* 恢复一个会话的历史消息,**喂给模型用**。
|
|
2444
|
-
*
|
|
2445
|
-
* 只取 user / assistant:tool 行是给宿主回放的,单独一条没有配对 tool-call 的
|
|
2446
|
-
* tool 消息会被 provider 拒掉。图片部件在这里被**读回 base64**,所以恢复出来的
|
|
2447
|
-
* 会话里模型仍然看得见上一轮那张图 —— 这就是方案 12 留下的「resume 恢复图片」。
|
|
2448
|
-
*
|
|
2449
|
-
* 图片文件找不到(artifact 清理跑过、用户手删)时退化成一句占位文本,不抛。
|
|
2450
|
-
*
|
|
2451
|
-
* ## ⚠️ 连续的 assistant 行在这里被**合并回一条**(2026-08-18)
|
|
2452
|
-
*
|
|
2453
|
-
* `record()` 改成一步一行之后,一轮在 DB 里会留下多条 assistant,中间垫着
|
|
2454
|
-
* tool 行。上面那道 user/assistant 过滤把 tool 行滤掉,剩下的就是**连着的几条
|
|
2455
|
-
* assistant**,其中还有「只调工具没说话」那种正文为空的。
|
|
2456
|
-
*
|
|
2457
|
-
* 两样都是没人验过的形状:`sdk-adapter` 会把 `content: ''` 原样发出去,而连着
|
|
2458
|
-
* 两条同角色消息在各家 provider 上的行为各不相同(仓里那个
|
|
2459
|
-
* `validateAlternation()` 干脆直接抛 —— 它今天没有生产调用方,但它写下的
|
|
2460
|
-
* 顾虑是真的)。
|
|
2461
|
-
*
|
|
2462
|
-
* 合并口径**按直播那一份来**:文本直接相接、不插分隔符(`run()` 里就是
|
|
2463
|
-
* `assistantText += event.text`),合完是空就整条不发(那边也是
|
|
2464
|
-
* `if (assistantText)`)。判据是 resume 回来的上下文必须和刚才在内存里的那份
|
|
2465
|
-
* **逐字相同** —— 差一个换行,模型看到的就是另一段历史。
|
|
2466
|
-
*/
|
|
2467
2450
|
loadHistory(sessionId) {
|
|
2468
2451
|
const prev = this.sessions.get(sessionId);
|
|
2469
2452
|
if (!prev) return [];
|
|
2470
2453
|
this.known.add(sessionId);
|
|
2471
2454
|
const out = [];
|
|
2455
|
+
let text = "";
|
|
2456
|
+
let results = [];
|
|
2457
|
+
let open = false;
|
|
2458
|
+
const flush = () => {
|
|
2459
|
+
if (!open) return;
|
|
2460
|
+
const content = assistantTurnText(text, results);
|
|
2461
|
+
if (content) out.push({ role: "assistant", content });
|
|
2462
|
+
text = "";
|
|
2463
|
+
results = [];
|
|
2464
|
+
open = false;
|
|
2465
|
+
};
|
|
2472
2466
|
for (const m of prev.messages) {
|
|
2473
2467
|
if (m.role === "user") {
|
|
2468
|
+
flush();
|
|
2474
2469
|
const parts = toParts(m.parts, true);
|
|
2475
2470
|
out.push({ role: "user", content: m.content, ...parts ? { parts } : {} });
|
|
2476
2471
|
continue;
|
|
2477
2472
|
}
|
|
2478
|
-
if (m.role
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2473
|
+
if (m.role === "assistant") {
|
|
2474
|
+
open = true;
|
|
2475
|
+
text += m.content;
|
|
2476
|
+
continue;
|
|
2477
|
+
}
|
|
2478
|
+
if (m.role === "tool") {
|
|
2479
|
+
if (open) results.push(...m.toolResults ?? []);
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
2482
|
}
|
|
2483
|
+
flush();
|
|
2483
2484
|
return out;
|
|
2484
2485
|
}
|
|
2485
|
-
/**
|
|
2486
|
-
* 恢复一个会话的**全部**消息,用于宿主回放(web 刷新页面、会话详情页)。
|
|
2487
|
-
*
|
|
2488
|
-
* 与 `loadHistory()` 的三点区别,都是「给人看」和「给模型看」的差别:
|
|
2489
|
-
* 1. 带 tool 行 —— 工具调用和 artifact 就挂在那上面
|
|
2490
|
-
* 2. 带 `id` / `timestamp`
|
|
2491
|
-
* 3. **图片不内联 base64**:`EpochImagePart.image` 里是绝对文件路径。
|
|
2492
|
-
* 宿主拿它拼自己的 artifact 端点,把几百 KB 的 base64 塞进一次 REST 响应
|
|
2493
|
-
* 或 SSE 是纯粹的浪费。工具产物同理:`ToolArtifact` 有 `path`、**没有** `data`。
|
|
2494
|
-
*
|
|
2495
|
-
* ⚠️ 因此这个方法的返回值**不能**直接喂给 provider。
|
|
2496
|
-
*/
|
|
2497
2486
|
loadMessages(sessionId) {
|
|
2498
2487
|
const prev = this.sessions.get(sessionId);
|
|
2499
2488
|
if (!prev) return [];
|
|
@@ -2504,13 +2493,8 @@ var init_session_store = __esm({
|
|
|
2504
2493
|
id: m.id,
|
|
2505
2494
|
timestamp: m.timestamp,
|
|
2506
2495
|
role: m.role,
|
|
2507
|
-
// protocol 的约定:tool 消息 content 为空串,内容在 toolResults 里,
|
|
2508
|
-
// 「取这条消息的文本」才永远不用判类型。DB 里那份是给 FTS 用的
|
|
2509
2496
|
content: m.role === "tool" ? "" : m.content,
|
|
2510
2497
|
...parts ? { parts } : {},
|
|
2511
|
-
// 思考原文和这一轮的账(2026-08-18)。**两样都只有回放这一支带** ——
|
|
2512
|
-
// `loadHistory()` 那份是喂模型的,模型不需要(也不该)看自己上一轮
|
|
2513
|
-
// 想过什么、花了多少钱
|
|
2514
2498
|
...m.reasoning ? { reasoning: m.reasoning } : {},
|
|
2515
2499
|
...m.turnFacts ? { turnFacts: m.turnFacts } : {},
|
|
2516
2500
|
...m.toolCalls ? { toolCalls: m.toolCalls } : {},
|
|
@@ -2518,15 +2502,6 @@ var init_session_store = __esm({
|
|
|
2518
2502
|
};
|
|
2519
2503
|
});
|
|
2520
2504
|
}
|
|
2521
|
-
/**
|
|
2522
|
-
* 这个会话在工作区里改过哪些文件(方案 42 PR-4)。
|
|
2523
|
-
*
|
|
2524
|
-
* @param workDir 相对路径按哪个根算。**这个会话绑定的工作区主根**(决定 18),
|
|
2525
|
-
* 不是服务进程的工作目录 —— 后者在多会话下只会是其中一个的,而它不说是哪个。
|
|
2526
|
-
*
|
|
2527
|
-
* 读不出来(SQLite 起不来 / 会话不存在)时是空数组,不抛:这是一个展示用的
|
|
2528
|
-
* 汇总,它自己坏了不该让整屏 500。
|
|
2529
|
-
*/
|
|
2530
2505
|
fileChanges(sessionId, workDir) {
|
|
2531
2506
|
try {
|
|
2532
2507
|
return collectFileChanges(this.loadMessages(sessionId), workDir);
|
|
@@ -2534,6 +2509,12 @@ var init_session_store = __esm({
|
|
|
2534
2509
|
return [];
|
|
2535
2510
|
}
|
|
2536
2511
|
}
|
|
2512
|
+
ftsHealth() {
|
|
2513
|
+
return this.sessions.ftsHealth();
|
|
2514
|
+
}
|
|
2515
|
+
reclaimFts(vacuum = true) {
|
|
2516
|
+
return this.sessions.reclaimFts(vacuum);
|
|
2517
|
+
}
|
|
2537
2518
|
};
|
|
2538
2519
|
}
|
|
2539
2520
|
});
|
|
@@ -2655,8 +2636,6 @@ async function applyMcpConfig(mcp, sync, opts) {
|
|
|
2655
2636
|
return {
|
|
2656
2637
|
ok: false,
|
|
2657
2638
|
reason: "no-servers",
|
|
2658
|
-
// 只带**第一条**诊断:屏幕上那句话后面已经跟着完整的诊断列表了,
|
|
2659
|
-
// 把十条拼成一行只会把它挤出去
|
|
2660
2639
|
detail: issues[0]?.message ?? ""
|
|
2661
2640
|
};
|
|
2662
2641
|
}
|
|
@@ -2882,8 +2861,6 @@ function writeMcpServer(input, opts = {}) {
|
|
|
2882
2861
|
return {
|
|
2883
2862
|
ok: false,
|
|
2884
2863
|
reason: "invalid-config",
|
|
2885
|
-
// 分隔符用 ASCII 的 `; `:这几条 message 本身是 plugin-mcp 那边的原文,
|
|
2886
|
-
// 我们只是把它们串起来,不该在中间插一个由**我们**的语言决定的标点
|
|
2887
2864
|
detail: probe.issues.map((i) => i.message).join("; ") || t("mcp_write.invalid")
|
|
2888
2865
|
};
|
|
2889
2866
|
}
|
|
@@ -2933,6 +2910,11 @@ var init_mcp_config_write = __esm({
|
|
|
2933
2910
|
EMPTY_CONFIG = '{\n "servers": {}\n}\n';
|
|
2934
2911
|
}
|
|
2935
2912
|
});
|
|
2913
|
+
function foldToolMode(mode) {
|
|
2914
|
+
if (mode === "both") return "both";
|
|
2915
|
+
if (mode === "code") return "code";
|
|
2916
|
+
return "tools";
|
|
2917
|
+
}
|
|
2936
2918
|
function applyShell(kind, diags) {
|
|
2937
2919
|
if (!kind || kind === "cmd") {
|
|
2938
2920
|
setShell(kind ?? null);
|
|
@@ -2945,29 +2927,34 @@ function applyShell(kind, diags) {
|
|
|
2945
2927
|
return;
|
|
2946
2928
|
}
|
|
2947
2929
|
setShell(kind);
|
|
2948
|
-
diags.ok("Shell",
|
|
2930
|
+
diags.ok("Shell", t("boot.shell_kind", { kind }));
|
|
2949
2931
|
}
|
|
2950
2932
|
function registerLocalTools(input) {
|
|
2951
|
-
const { trackerDb, goals, memory, sandbox, diags } = input;
|
|
2952
|
-
const registry = new ToolRegistry();
|
|
2933
|
+
const { trackerDb, goals, memory, sandbox, skills, diags } = input;
|
|
2934
|
+
const registry = new ToolRegistry({ codeMode: input.toolMode === "code" });
|
|
2953
2935
|
for (const plugin of [filePlugin, terminalPlugin, webPlugin, lspPlugin]) {
|
|
2954
2936
|
try {
|
|
2955
2937
|
registry.registerPlugin(plugin);
|
|
2956
2938
|
} catch (err) {
|
|
2957
2939
|
diags.failed(
|
|
2958
2940
|
`Plugin ${plugin.name}`,
|
|
2959
|
-
|
|
2941
|
+
t("boot.plugin_register_failed", {
|
|
2942
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
2943
|
+
}),
|
|
2960
2944
|
"plugin-register-failed"
|
|
2961
2945
|
);
|
|
2962
2946
|
}
|
|
2963
2947
|
}
|
|
2964
2948
|
const rg = resolveRipgrep();
|
|
2965
2949
|
if (rg.mode === "missing") {
|
|
2966
|
-
diags.warn("Ripgrep",
|
|
2950
|
+
diags.warn("Ripgrep", t("boot.ripgrep_missing", { hint: rg.hint ?? "" }), "ripgrep-missing");
|
|
2967
2951
|
} else if (rg.mode === "system") {
|
|
2968
|
-
diags.ok("Ripgrep", "
|
|
2952
|
+
diags.ok("Ripgrep", t("boot.ripgrep_system"));
|
|
2969
2953
|
} else {
|
|
2970
|
-
diags.ok(
|
|
2954
|
+
diags.ok(
|
|
2955
|
+
"Ripgrep",
|
|
2956
|
+
t("boot.ripgrep_builtin", { version: rg.version === void 0 ? "" : ` ${rg.version}` })
|
|
2957
|
+
);
|
|
2971
2958
|
}
|
|
2972
2959
|
const search = selectSearchProvider(input.search);
|
|
2973
2960
|
if (search.provider) {
|
|
@@ -2977,16 +2964,46 @@ function registerLocalTools(input) {
|
|
|
2977
2964
|
input.search?.maxResults ? { defaultMaxResults: input.search.maxResults } : {}
|
|
2978
2965
|
)
|
|
2979
2966
|
);
|
|
2980
|
-
diags.ok("
|
|
2967
|
+
diags.ok("WebSearch", search.detail);
|
|
2981
2968
|
} else {
|
|
2982
|
-
diags.skipped("
|
|
2969
|
+
diags.skipped("WebSearch", search.detail);
|
|
2983
2970
|
}
|
|
2984
2971
|
if (trackerDb) registry.registerTool(createTodoTool(trackerDb));
|
|
2985
2972
|
if (goals) registry.registerTool(createGoalTool({ service: goals }));
|
|
2986
2973
|
if (memory) registry.registerTool(createMemoryTool(memory));
|
|
2987
2974
|
if (sandbox) registry.registerTool(createCodeExecTool(sandbox));
|
|
2975
|
+
if (input.toolMode === "both" || input.toolMode === "code") {
|
|
2976
|
+
registry.registerTool(createRunCodeTool());
|
|
2977
|
+
}
|
|
2978
|
+
if (skills) registry.registerTool(createSkillViewTool({ skills }));
|
|
2979
|
+
if (input.deferMcp === true) {
|
|
2980
|
+
registry.registerTool(
|
|
2981
|
+
createToolSearchTool({
|
|
2982
|
+
deferred: () => registry.byExposure(ToolExposure.Deferred),
|
|
2983
|
+
reveal: input.bootstrapReveal ?? createToolRevealState()
|
|
2984
|
+
})
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2988
2987
|
return registry;
|
|
2989
2988
|
}
|
|
2989
|
+
function reportToolCost(registry, diags) {
|
|
2990
|
+
const exposed = registry.exposed();
|
|
2991
|
+
const mcp = exposed.filter(
|
|
2992
|
+
(tool) => (registry.sourceOf(tool.name) ?? "").startsWith("mcp:")
|
|
2993
|
+
).length;
|
|
2994
|
+
const local = exposed.length - mcp;
|
|
2995
|
+
const tokens = Math.round(toolTableTokens(exposed).total / 100) / 10;
|
|
2996
|
+
const split = (lang) => mcp > 0 ? t("boot.tools_split_mixed", { local, mcp }, lang) : t("boot.tools_split_local", { local }, lang);
|
|
2997
|
+
const deferred = registry.byExposure(ToolExposure.Deferred).length;
|
|
2998
|
+
diags.ok(
|
|
2999
|
+
TOOLS_MODULE,
|
|
3000
|
+
(lang) => deferred > 0 ? t(
|
|
3001
|
+
"boot.tools_cost_deferred",
|
|
3002
|
+
{ count: exposed.length, split: split(lang), tokens, deferred },
|
|
3003
|
+
lang
|
|
3004
|
+
) : t("boot.tools_cost", { count: exposed.length, split: split(lang), tokens }, lang)
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
2990
3007
|
function registerSessionSearch(registry, input) {
|
|
2991
3008
|
if (!input.session) {
|
|
2992
3009
|
input.diags.skipped(
|
|
@@ -3030,7 +3047,13 @@ function registerAskQuestion(registry, onQuestionRequest, interactive) {
|
|
|
3030
3047
|
createAskQuestionTool(interactive ? { requestQuestion: onQuestionRequest } : {})
|
|
3031
3048
|
);
|
|
3032
3049
|
}
|
|
3033
|
-
function
|
|
3050
|
+
function registerMcpBatch(registry, tools, server, defer) {
|
|
3051
|
+
registry.registerTools(
|
|
3052
|
+
defer ? tools.map((tool) => ({ ...tool, exposure: ToolExposure.Deferred })) : [...tools],
|
|
3053
|
+
`mcp:${server}`
|
|
3054
|
+
);
|
|
3055
|
+
}
|
|
3056
|
+
function createToolSync(mcp, registry, defer) {
|
|
3034
3057
|
let queue = Promise.resolve();
|
|
3035
3058
|
const replace = (server) => {
|
|
3036
3059
|
queue = queue.then(async () => {
|
|
@@ -3038,9 +3061,11 @@ function createToolSync(mcp, registry) {
|
|
|
3038
3061
|
const fresh = await withTimeout(mcp.refresh(server), MCP_LIST_BUDGET_MS, null) ?? [];
|
|
3039
3062
|
const prefix = `mcp__${server}__`;
|
|
3040
3063
|
registry.unregisterBySource(`mcp:${server}`);
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3064
|
+
registerMcpBatch(
|
|
3065
|
+
registry,
|
|
3066
|
+
fresh.filter((t23) => t23.name.startsWith(prefix)),
|
|
3067
|
+
server,
|
|
3068
|
+
defer
|
|
3044
3069
|
);
|
|
3045
3070
|
} catch {
|
|
3046
3071
|
}
|
|
@@ -3049,7 +3074,7 @@ function createToolSync(mcp, registry) {
|
|
|
3049
3074
|
};
|
|
3050
3075
|
return { settled: () => queue, replace };
|
|
3051
3076
|
}
|
|
3052
|
-
function wireToolRefresh(mcp, registry, sync = createToolSync(mcp, registry)) {
|
|
3077
|
+
function wireToolRefresh(mcp, registry, sync = createToolSync(mcp, registry, false)) {
|
|
3053
3078
|
mcp.onToolsChanged = (server) => void sync.replace(server);
|
|
3054
3079
|
return sync;
|
|
3055
3080
|
}
|
|
@@ -3098,8 +3123,6 @@ async function addServer(mcp, sync, input, homeDir) {
|
|
|
3098
3123
|
return {
|
|
3099
3124
|
ok: true,
|
|
3100
3125
|
configPath: written.configPath,
|
|
3101
|
-
// `connectAll` 恒回一台一条,兜底那一支是给类型系统看的 —— 真走到了说明
|
|
3102
|
-
// registry 那边变了形状,那时报一台「连不上、没说为什么」比崩掉强
|
|
3103
3126
|
status: status ?? mcp.getStatus(input.name) ?? {
|
|
3104
3127
|
name: input.name,
|
|
3105
3128
|
connected: false,
|
|
@@ -3142,9 +3165,9 @@ function reportTaken(source, name, diags) {
|
|
|
3142
3165
|
"host-mcp-name-taken"
|
|
3143
3166
|
);
|
|
3144
3167
|
}
|
|
3145
|
-
async function registerMcpTools(registry, homeDir, diags, injected = []) {
|
|
3168
|
+
async function registerMcpTools(registry, homeDir, diags, injected = [], defer = false) {
|
|
3146
3169
|
const mcp = new McpRegistry({ homeDir });
|
|
3147
|
-
const sync = createToolSync(mcp, registry);
|
|
3170
|
+
const sync = createToolSync(mcp, registry, defer);
|
|
3148
3171
|
const live = () => ({
|
|
3149
3172
|
dispose: () => void mcp.disconnectAll().catch(() => {
|
|
3150
3173
|
}),
|
|
@@ -3176,19 +3199,19 @@ async function registerMcpTools(registry, homeDir, diags, injected = []) {
|
|
|
3176
3199
|
try {
|
|
3177
3200
|
const { servers, issues } = mcp.loadFromConfig(mcpConfigPath(homeDir));
|
|
3178
3201
|
for (const issue of issues) {
|
|
3179
|
-
diags.warn(`
|
|
3202
|
+
diags.warn(`MCPConfig ${issue.path}`, issue.message, "mcp-config-invalid");
|
|
3180
3203
|
}
|
|
3181
3204
|
const extra = dropTakenNames(injected, new Set(servers.map((s) => s.name)), diags);
|
|
3182
3205
|
const injectedCount = extra.reduce((sum, b) => sum + b.servers.length, 0);
|
|
3183
3206
|
if (servers.length === 0 && injectedCount === 0) {
|
|
3184
|
-
diags.skipped("MCP", "
|
|
3207
|
+
diags.skipped("MCP", t("boot.mcp_none"));
|
|
3185
3208
|
return live();
|
|
3186
3209
|
}
|
|
3187
3210
|
const statuses = await connectAllServers(mcp, [{ source: "user", servers }, ...extra]);
|
|
3188
3211
|
if (statuses === null) {
|
|
3189
3212
|
diags.failed(
|
|
3190
3213
|
"MCP",
|
|
3191
|
-
|
|
3214
|
+
t("boot.mcp_connect_timeout", { seconds: MCP_CONNECT_BUDGET_MS / 1e3 }),
|
|
3192
3215
|
"mcp-connect-timeout"
|
|
3193
3216
|
);
|
|
3194
3217
|
return live();
|
|
@@ -3198,14 +3221,22 @@ async function registerMcpTools(registry, homeDir, diags, injected = []) {
|
|
|
3198
3221
|
if (!st.connected) {
|
|
3199
3222
|
diags.failed(
|
|
3200
3223
|
`MCP ${st.name}`,
|
|
3201
|
-
|
|
3224
|
+
t("boot.mcp_connect_failed", {
|
|
3225
|
+
detail: st.lastError ? t("boot.mcp_connect_failed_detail", { detail: st.lastError }) : ""
|
|
3226
|
+
}),
|
|
3202
3227
|
"mcp-connect-failed"
|
|
3203
3228
|
);
|
|
3204
3229
|
continue;
|
|
3205
3230
|
}
|
|
3206
|
-
const own = mcpTools.filter((
|
|
3207
|
-
|
|
3208
|
-
|
|
3231
|
+
const own = mcpTools.filter((t23) => t23.name.startsWith(`mcp__${st.name}__`));
|
|
3232
|
+
const registered = dropOverlongToolNames(
|
|
3233
|
+
own,
|
|
3234
|
+
`MCP ${st.name}`,
|
|
3235
|
+
"mcp-tool-name-too-long",
|
|
3236
|
+
diags
|
|
3237
|
+
);
|
|
3238
|
+
registerMcpBatch(registry, registered, st.name, defer);
|
|
3239
|
+
diags.ok(`MCP ${st.name}`, t("boot.mcp_tools", { count: registered.length }));
|
|
3209
3240
|
if (st.lastError) diags.warn(`MCP ${st.name}`, st.lastError, "mcp-degraded");
|
|
3210
3241
|
}
|
|
3211
3242
|
wireToolRefresh(mcp, registry, sync);
|
|
@@ -3213,13 +3244,15 @@ async function registerMcpTools(registry, homeDir, diags, injected = []) {
|
|
|
3213
3244
|
} catch (err) {
|
|
3214
3245
|
diags.failed(
|
|
3215
3246
|
"MCP",
|
|
3216
|
-
|
|
3247
|
+
t("boot.mcp_init_failed", {
|
|
3248
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
3249
|
+
}),
|
|
3217
3250
|
"mcp-init-failed"
|
|
3218
3251
|
);
|
|
3219
3252
|
return live();
|
|
3220
3253
|
}
|
|
3221
3254
|
}
|
|
3222
|
-
var MCP_CONNECT_BUDGET_MS, MCP_LIST_BUDGET_MS, SESSION_SEARCH_MODULE;
|
|
3255
|
+
var MCP_CONNECT_BUDGET_MS, MCP_LIST_BUDGET_MS, TOOLS_MODULE, SESSION_SEARCH_MODULE;
|
|
3223
3256
|
var init_tools = __esm({
|
|
3224
3257
|
"src/tools.ts"() {
|
|
3225
3258
|
init_host_capabilities();
|
|
@@ -3228,6 +3261,7 @@ var init_tools = __esm({
|
|
|
3228
3261
|
init_mcp_config_write();
|
|
3229
3262
|
MCP_CONNECT_BUDGET_MS = 15e3;
|
|
3230
3263
|
MCP_LIST_BUDGET_MS = 1e4;
|
|
3264
|
+
TOOLS_MODULE = "Tools";
|
|
3231
3265
|
SESSION_SEARCH_MODULE = "SessionSearch";
|
|
3232
3266
|
}
|
|
3233
3267
|
});
|
|
@@ -3271,11 +3305,6 @@ var init_session_factory = __esm({
|
|
|
3271
3305
|
SessionBridges = class {
|
|
3272
3306
|
byId = /* @__PURE__ */ new Map();
|
|
3273
3307
|
override;
|
|
3274
|
-
/**
|
|
3275
|
-
* @param override 宿主自己实现的阻塞式确认(`BuildRuntimeOptions.onApprovalRequest`)。
|
|
3276
|
-
* 给了就**所有会话都走它**:那种宿主(CLI 的 inquirer)本来就是单会话的,
|
|
3277
|
-
* 它的回调签名里也没有 sessionId 的位置。
|
|
3278
|
-
*/
|
|
3279
3308
|
constructor(override) {
|
|
3280
3309
|
this.override = override;
|
|
3281
3310
|
}
|
|
@@ -3285,32 +3314,11 @@ var init_session_factory = __esm({
|
|
|
3285
3314
|
release(sessionId) {
|
|
3286
3315
|
this.byId.delete(sessionId);
|
|
3287
3316
|
}
|
|
3288
|
-
/*
|
|
3289
|
-
* ⚠️ 这儿原来有一个 `planOwner()`(记「最后一次交计划的是谁」),
|
|
3290
|
-
* 2026-08-16 **删掉**了。它存在的唯一理由是 `PlanModeState.onChange` 上没有
|
|
3291
|
-
* 会话 id —— 因为那时候 plan 模式是**进程级**的一个状态对象。现在一个会话一个
|
|
3292
|
-
* 状态,`onChange` 的宿主自己就知道自己是谁。
|
|
3293
|
-
*
|
|
3294
|
-
* **不是留着不调,是必须删**:两个会话各自挂着落盘 hook、而引导会话那一份还
|
|
3295
|
-
* 按 `planOwner()` 决定写到谁名下的话,会话 B 交过一次计划之后,引导会话再批
|
|
3296
|
-
* 一份就会把 A 的正文写到 B 名下。判据全文在 `session-guard.ts` 的文件头末节。
|
|
3297
|
-
*/
|
|
3298
|
-
/**
|
|
3299
|
-
* 转一条审批给它该去的那个会话。
|
|
3300
|
-
*
|
|
3301
|
-
* 认不出这个 sessionId(会话刚被删 / 被冷却,而它那一轮还在收尾)时回 `deny` ——
|
|
3302
|
-
* 方向逐字同 `AgentSession` 的 `settleAbandoned`:审批的安全侧就是拒绝。
|
|
3303
|
-
* 挂着不答的话,那个孤儿 `AgentLoop` 会永远停在 await 上,进程退不掉。
|
|
3304
|
-
*/
|
|
3305
3317
|
approve(sessionId, req) {
|
|
3306
3318
|
if (this.override) return this.override(req);
|
|
3307
3319
|
const bridge = this.byId.get(sessionId);
|
|
3308
3320
|
return bridge ? bridge.approve(req) : Promise.resolve("deny");
|
|
3309
3321
|
}
|
|
3310
|
-
/**
|
|
3311
|
-
* 转一条提问。认不出时回 `skipped` —— 那是**实话**(确实没人答),
|
|
3312
|
-
* 而替用户挑一个 label 才是伪造(方案 34 §2.4)。
|
|
3313
|
-
*/
|
|
3314
3322
|
ask(sessionId, req) {
|
|
3315
3323
|
const bridge = this.byId.get(sessionId);
|
|
3316
3324
|
return bridge ? bridge.ask(req) : Promise.resolve({ answers: {}, skipped: true });
|
|
@@ -3334,82 +3342,27 @@ var init_session_factory = __esm({
|
|
|
3334
3342
|
get(sessionId) {
|
|
3335
3343
|
return this.live.get(sessionId) ?? null;
|
|
3336
3344
|
}
|
|
3337
|
-
/**
|
|
3338
|
-
* 把**引导会话**装进表里。装配层专用,`bind()` 那两道门在这条路上不走 ——
|
|
3339
|
-
* 它的工作区已经 `seed()` 进去了(判据见 workspace.ts 的 `seed`)。
|
|
3340
|
-
*
|
|
3341
|
-
* @returns 建不出 `AgentLoop`(provider 没起来)时 null。那不是错误:
|
|
3342
|
-
* 一台还没配 API key 的机器上跑 `epoch doctor` 走的就是这条路
|
|
3343
|
-
*/
|
|
3344
3345
|
adopt(sessionId, history) {
|
|
3345
3346
|
this.bootstrap = sessionId;
|
|
3346
|
-
const live = this.assemble(sessionId,
|
|
3347
|
+
const live = this.assemble(sessionId, history, false);
|
|
3347
3348
|
if (live) this.live.set(sessionId, live);
|
|
3348
3349
|
return live;
|
|
3349
3350
|
}
|
|
3350
|
-
/**
|
|
3351
|
-
* `/resume` 换了 id 之后把表跟着改一下(`SessionControl.resume`)。
|
|
3352
|
-
*
|
|
3353
|
-
* `AgentSession.resume()` 自己会把 `sessionId` 换掉,但**这张表的键不会跟着变** ——
|
|
3354
|
-
* 不补这一步的话 `get(新 id)` 是 null,于是浏览器刚 resume 过去的那个会话
|
|
3355
|
-
* 在这个进程眼里不存在。
|
|
3356
|
-
*
|
|
3357
|
-
* ## ⚠️ 搬的是**两格**,桥那一格 2026-08-18 才补上
|
|
3358
|
-
*
|
|
3359
|
-
* 原来这儿只有 `bridges.release(from)`,没有按新键 `register` 一次
|
|
3360
|
-
* (`register` 只出现在建会话那一条路上,见 {@link assemble})。于是 resume
|
|
3361
|
-
* 之后新 id 在路由表里是空的,而**后果是静默答错不是报错**:
|
|
3362
|
-
* `ask_user_question` 回 `skipped`、plan 审批回 `deny` —— 模型问不出问题、
|
|
3363
|
-
* 批不了计划,屏幕上只是「它没问」。
|
|
3364
|
-
*
|
|
3365
|
-
* **这不是 2026-08-17 那轮 `from` 取早一步的修复改出来的**:那之前
|
|
3366
|
-
* `ToolContext.sessionId` 已经是新 id 而桥登记在旧 id 上,一样查不到。
|
|
3367
|
-
* 两种情形下新 id 都无桥,那一轮既没让它变坏也没变好。
|
|
3368
|
-
*
|
|
3369
|
-
* ⚠️ **不是「只有 web 才踩」**(2026-08-18 实测修正的一处口径)。`/resume`
|
|
3370
|
-
* 今天确实只有 TUI 走得到,而 TUI 上三条路**分成两档**:
|
|
3371
|
-
*
|
|
3372
|
-
* - 普通工具的权限审批 **不走**这一层(`onApprovalRequest` 直接给这个会话
|
|
3373
|
-
* 自己那座桥,见 {@link assemble} 那条注释)—— 那一档确实没事;
|
|
3374
|
-
* - `ask_user_question` 和 `exit_plan_mode` 交计划 **走**(`build.ts` 里
|
|
3375
|
-
* 那两句 `(req, sid) => bridges.ask/approve(sid, req)`)—— 这两样 TUI
|
|
3376
|
-
* `/resume` 之后一样是坏的。判据:`ToolContext.sessionId` 来自
|
|
3377
|
-
* `AgentLoop` 的 `input.sessionId`,也就是换过之后那个新 id。
|
|
3378
|
-
*
|
|
3379
|
-
* ## 判了哪条治法:**把建会话那一刻那份桥原样 register 到新键上**
|
|
3380
|
-
*
|
|
3381
|
-
* 判据只有一条,而它是**读出来的不是推的**:{@link
|
|
3382
|
-
* AgentSession.createApprovalBridge} 那两个回调闭的是 `target`(那个
|
|
3383
|
-
* `AgentSession` **实例**)和一个 `buffered` 数组,**一个 id 都没闭** ——
|
|
3384
|
-
* 没有 sessionId、没有工作区、没有 workDir。而 `AgentSession.resume()` 是
|
|
3385
|
-
* **就地**改 `this.sessionId`(实例不换、`attach` 过的 `target` 还是它)。
|
|
3386
|
-
* 所以「同一份桥」指的就是「用户此刻在看的那一段会话」,不是一个过期快照。
|
|
3387
|
-
*
|
|
3388
|
-
* 这也顺带答了「resume 过去之后这些回调该指向谁」——**照旧指向这一头**。
|
|
3389
|
-
* 判据同 `build.ts` 那个回调里 `workspaces.seed` / `rekeyBackgroundTasks`
|
|
3390
|
-
* 两行(2026-08-17 写的):`/resume` 只是把另一段对话接上**这个进程的这个
|
|
3391
|
-
* 引擎**,问答通道的另一头照旧是这个进程的宿主(TUI 的屏幕 / 浏览器那条 SSE)。
|
|
3392
|
-
* 「那段会话当初那个宿主」根本不存在 —— 那个进程早退了。
|
|
3393
|
-
*
|
|
3394
|
-
* **被否掉的另一条:在这里 `createApprovalBridge()` 新建一座登记上去。**
|
|
3395
|
-
* 它比今天更坏,两处:① `attach()` 绑的是新闭包里那个 `target`,而正在跑的
|
|
3396
|
-
* 那个 `AgentSession` 已经和旧桥绑好了 —— 新桥的请求会落进一条**没有消费者**
|
|
3397
|
-
* 的 `buffered`,于是 `ask` 从「回 `skipped`」变成「永远不返回」,那个
|
|
3398
|
-
* `AgentLoop` 挂在 await 上,进程退不掉;② 旧桥上**已经在飞**的请求
|
|
3399
|
-
* (`inFlight` 里那些)连带失去归属。一句话:桥不是按 id 建的,
|
|
3400
|
-
* 所以换 id 这件事上它压根不该重建。
|
|
3401
|
-
*/
|
|
3402
3351
|
rekey(from, to) {
|
|
3403
3352
|
const live = this.live.get(from);
|
|
3404
3353
|
if (!live || from === to) return;
|
|
3405
3354
|
this.live.delete(from);
|
|
3406
3355
|
this.opts.bridges.release(from);
|
|
3356
|
+
const workspaces = this.opts.workspaces;
|
|
3407
3357
|
const moved = {
|
|
3408
3358
|
...live,
|
|
3409
3359
|
sessionId: to,
|
|
3410
|
-
workspace
|
|
3360
|
+
get workspace() {
|
|
3361
|
+
return workspaces.of(to);
|
|
3362
|
+
}
|
|
3411
3363
|
};
|
|
3412
3364
|
this.live.set(to, moved);
|
|
3365
|
+
this.opts.processes.claim(to);
|
|
3413
3366
|
this.opts.bridges.register(to, moved.bridge);
|
|
3414
3367
|
if (this.bootstrap === from) this.bootstrap = to;
|
|
3415
3368
|
}
|
|
@@ -3425,15 +3378,13 @@ var init_session_factory = __esm({
|
|
|
3425
3378
|
if (hit) return { ok: true, live: hit, created: false };
|
|
3426
3379
|
}
|
|
3427
3380
|
const sessionId = this.newId();
|
|
3428
|
-
let bound = null;
|
|
3429
3381
|
if (input.workspace !== null) {
|
|
3430
3382
|
const outcome = this.opts.workspaces.bind(sessionId, input.workspace, input.lang);
|
|
3431
3383
|
if (!outcome.ok) return { ok: false, reason: outcome.reason, detail: outcome.detail };
|
|
3432
|
-
bound = outcome.bound;
|
|
3433
3384
|
} else {
|
|
3434
3385
|
this.opts.workspaces.decideNone(sessionId);
|
|
3435
3386
|
}
|
|
3436
|
-
const live = this.assemble(sessionId,
|
|
3387
|
+
const live = this.assemble(sessionId, [], true, role);
|
|
3437
3388
|
if (!live) {
|
|
3438
3389
|
this.opts.workspaces.release(sessionId);
|
|
3439
3390
|
return { ok: false, reason: "unavailable" };
|
|
@@ -3441,7 +3392,6 @@ var init_session_factory = __esm({
|
|
|
3441
3392
|
this.live.set(sessionId, live);
|
|
3442
3393
|
return { ok: true, live, created: true };
|
|
3443
3394
|
}
|
|
3444
|
-
/** 判据全文在 {@link SessionFactory.revive} 上 */
|
|
3445
3395
|
revive(input) {
|
|
3446
3396
|
const { sessionId } = input;
|
|
3447
3397
|
const already = this.live.get(sessionId);
|
|
@@ -3451,15 +3401,13 @@ var init_session_factory = __esm({
|
|
|
3451
3401
|
const decision = remembered ?? input.decision;
|
|
3452
3402
|
const awaitingPick = this.opts.workspaces.stateOf(sessionId) === "unbound";
|
|
3453
3403
|
if (!decision && !awaitingPick) return { ok: false, reason: "no-decision" };
|
|
3454
|
-
let bound = null;
|
|
3455
3404
|
if (decision?.state === "bound") {
|
|
3456
3405
|
const outcome = this.opts.workspaces.bind(sessionId, decision.root, input.lang);
|
|
3457
3406
|
if (!outcome.ok) return { ok: false, reason: outcome.reason, detail: outcome.detail };
|
|
3458
|
-
bound = outcome.bound;
|
|
3459
3407
|
} else if (decision) {
|
|
3460
3408
|
this.opts.workspaces.decideNone(sessionId);
|
|
3461
3409
|
}
|
|
3462
|
-
const live = this.assemble(sessionId,
|
|
3410
|
+
const live = this.assemble(sessionId, [], true);
|
|
3463
3411
|
if (!live) {
|
|
3464
3412
|
if (!remembered && !awaitingPick) this.opts.workspaces.release(sessionId);
|
|
3465
3413
|
return { ok: false, reason: "unavailable" };
|
|
@@ -3472,67 +3420,40 @@ var init_session_factory = __esm({
|
|
|
3472
3420
|
this.live.delete(sessionId);
|
|
3473
3421
|
this.opts.bridges.release(sessionId);
|
|
3474
3422
|
}
|
|
3475
|
-
/**
|
|
3476
|
-
* 一个 `delegate_task` 子跑手。**全进程一个**,按 `parentSessionId` 现查现配。
|
|
3477
|
-
*
|
|
3478
|
-
* 三处 id 必须都是**派活那个会话**的,缺一处就是 §6.3 第 3 条那个洞:
|
|
3479
|
-
* `budgetSessionId`(钱记谁头上)、两个 `checkpoints` 回调(快照落谁的目录)、
|
|
3480
|
-
* 以及审批弹给谁看。在这里现查而不是装配时闭包一个,正是因为那个闭包
|
|
3481
|
-
* 捕获的是引导会话。
|
|
3482
|
-
*
|
|
3483
|
-
* @param provider **收参数而不是读 `this.opts.provider`**:`delegate_task`
|
|
3484
|
-
* 只在 provider 起得来时才注册,收进来之后这条路上就没有「provider 是 null」
|
|
3485
|
-
* 这个状态了 —— 少一个只能靠一句话解释的不可达分支。
|
|
3486
|
-
*
|
|
3487
|
-
* ⚠️ 它今天只是**兜底**(派活的会话已经被冷却掉了):2026-08-16 起子 agent
|
|
3488
|
-
* 跑在**派活那个会话**的 provider 门面上,理由见下面那两行注释
|
|
3489
|
-
*/
|
|
3490
3423
|
subRunner(provider) {
|
|
3491
3424
|
return async (goal, context, subSessionId, roleName, parentSessionId) => {
|
|
3492
3425
|
const { services, config } = this.opts;
|
|
3493
3426
|
const parent = this.live.get(parentSessionId);
|
|
3494
3427
|
const workDir = parent?.workspace?.workspace.root ?? this.opts.defaultWorkDir;
|
|
3495
3428
|
const projectContext = parent?.workspace?.projectContext ?? services.projectContext;
|
|
3496
|
-
const checkpoints = new CheckpointManager({
|
|
3429
|
+
const checkpoints = new CheckpointManager({
|
|
3430
|
+
workDir: () => workDir,
|
|
3431
|
+
homeDir: config.homeDir
|
|
3432
|
+
});
|
|
3497
3433
|
const role = findRole(this.opts.roles(), roleName ?? DEFAULT_AGENT_ROLE);
|
|
3498
3434
|
const permission = parent?.permission ?? services.permission;
|
|
3499
3435
|
const sub = new AgentLoop({
|
|
3500
|
-
// 跑在**派活那个会话**的模型上(2026-08-16)。和下面 `budgetSessionId` /
|
|
3501
|
-
// `checkpoints` / `onApprovalRequest` 是同一条判据:子任务是那个会话的
|
|
3502
|
-
// 一部分。读进程那一份的话,会话 B 明明换到了别的模型,它派出去的活
|
|
3503
|
-
// 还在打引导会话那个 —— 而账是记在 B 头上的
|
|
3504
3436
|
provider: parent?.provider ?? provider,
|
|
3505
3437
|
toolProvider: roleScopedProvider(this.opts.rawToolProvider, role, BLOCKED_TOOLS),
|
|
3438
|
+
codeModeTools: this.opts.codeModeTools,
|
|
3506
3439
|
telemetry: this.opts.telemetry,
|
|
3507
3440
|
workDir,
|
|
3508
|
-
// 复用父会话那份:信任判定和指令文件已经算过、也已经进过诊断,
|
|
3509
|
-
// 每个子 agent 再算一遍只是重复读盘和重复跑 git
|
|
3510
3441
|
projectContext,
|
|
3511
|
-
// 角色说了算,没说才回落到「父的一半」——避免单个子任务吃掉全部预算
|
|
3512
3442
|
maxTurns: role?.maxTurns ?? Math.max(4, Math.floor(config.maxTurns / 2)),
|
|
3513
3443
|
contextLength: services.contextLength,
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
// 和它上面十几行「原样透传」的写法长得不一样,反而像在做别的事
|
|
3517
|
-
//
|
|
3518
|
-
// 这条路上**没有槽**(方案 57 §3.2 只换了 `assemble()` 那一行):一个子任务
|
|
3519
|
-
// 从派出去到跑完就是一次 run,中途没有「下一条消息」这个概念。写成函数
|
|
3520
|
-
// 只是跟着 `AgentConfig.role` 的形状走,它每次都回同一份对象
|
|
3444
|
+
artifactRetention: resolveArtifactRetention(config.artifacts),
|
|
3445
|
+
sandboxTerminal: config.sandbox?.terminal !== false,
|
|
3521
3446
|
...role ? { role: () => ({ name: role.name, ...role.prompt ? { prompt: role.prompt } : {} }) } : {},
|
|
3522
3447
|
...permission ? { permissionManager: permission } : {},
|
|
3523
3448
|
...services.hookManager ? { hookManager: services.hookManager } : {},
|
|
3449
|
+
skillSystem: services.skill ?? void 0,
|
|
3450
|
+
roleSkills: () => role?.skills,
|
|
3524
3451
|
...this.opts.budget,
|
|
3525
|
-
// 子任务的花费记到**派活的那个会话**头上。不这么做的话 `delegate_task`
|
|
3526
|
-
// 就是一条绕过成本上限的通道;而记到引导会话头上(工厂之前那样)更糟 ——
|
|
3527
|
-
// 那是**另一个人**的预算
|
|
3528
3452
|
budgetSessionId: parentSessionId,
|
|
3529
|
-
// 同上:子 agent 改的文件要能被派活那个会话的 `/rewind` 看见
|
|
3530
3453
|
checkpoints: {
|
|
3531
3454
|
capture: (_sub, paths) => checkpoints.capture(parentSessionId, paths),
|
|
3532
3455
|
record: (_sub, paths) => checkpoints.record(parentSessionId, paths)
|
|
3533
3456
|
},
|
|
3534
|
-
// 审批弹给派活那个会话的界面。`BLOCKED_TOOLS` 挡掉了 plan 和提问那几个,
|
|
3535
|
-
// 剩下的写类工具照样要问人
|
|
3536
3457
|
onApprovalRequest: (req) => this.opts.bridges.approve(parentSessionId, req)
|
|
3537
3458
|
});
|
|
3538
3459
|
let r;
|
|
@@ -3554,23 +3475,9 @@ ${context}` : goal,
|
|
|
3554
3475
|
};
|
|
3555
3476
|
};
|
|
3556
3477
|
}
|
|
3557
|
-
|
|
3558
|
-
// 私有:全仓唯一一处「主会话的 `new AgentLoop` + `new AgentSession`」
|
|
3559
|
-
// -------------------------------------------------------------------------
|
|
3560
|
-
/**
|
|
3561
|
-
* @param own 这个会话要不要**自己开一份**模型 / 权限档 / plan 模式
|
|
3562
|
-
* (2026-08-16)。引导会话是 `false` —— 它拿的就是进程那两份本体。
|
|
3563
|
-
*
|
|
3564
|
-
* **收一个布尔而不是在这里判 `sessionId === this.bootstrap`**:`adopt()`
|
|
3565
|
-
* 恰好在给 `this.bootstrap` 赋值的那一行之后调进来,照那个字段判的话,
|
|
3566
|
-
* 这段代码的正确性靠的是两行之间的时序 —— 而下一个人挪一行就会把它挪坏
|
|
3567
|
-
* @param role **这个会话**的专家(决定 20 ②)。不给就回落到 `--agent` 那一份
|
|
3568
|
-
* (`opts.mainRole`,进程级)—— 两者形状完全一样,走的也是同一条收窄路,
|
|
3569
|
-
* 区别只在作用域。回落而不是二选一:`epoch web --agent explore` 之后新建的
|
|
3570
|
-
* 会话仍然该是 explore,那是用户在命令行上说过的话。
|
|
3571
|
-
*/
|
|
3572
|
-
assemble(sessionId, bound, history, own, role) {
|
|
3478
|
+
assemble(sessionId, history, own, role) {
|
|
3573
3479
|
const opts = this.opts;
|
|
3480
|
+
opts.processes.claim(sessionId);
|
|
3574
3481
|
const { services, config, provider, models } = opts;
|
|
3575
3482
|
if (!provider || !models) return null;
|
|
3576
3483
|
const scope = {
|
|
@@ -3579,14 +3486,26 @@ ${context}` : goal,
|
|
|
3579
3486
|
};
|
|
3580
3487
|
const active = role ?? opts.mainRole ?? void 0;
|
|
3581
3488
|
const roleScope = createRoleScope(
|
|
3582
|
-
withSessionTools(
|
|
3489
|
+
withSessionTools(
|
|
3490
|
+
revealScopedProvider(opts.toolProvider, opts.deferredTools, scope.guard.reveal),
|
|
3491
|
+
scope.guard.tools
|
|
3492
|
+
),
|
|
3583
3493
|
active,
|
|
3584
3494
|
[]
|
|
3585
3495
|
);
|
|
3586
3496
|
const planState = scope.guard.planState;
|
|
3587
|
-
const
|
|
3588
|
-
const
|
|
3589
|
-
|
|
3497
|
+
const live = {};
|
|
3498
|
+
const site = () => {
|
|
3499
|
+
const now = opts.workspaces.of(live.session?.sessionId ?? sessionId);
|
|
3500
|
+
return {
|
|
3501
|
+
workDir: now?.workspace.root ?? opts.defaultWorkDir,
|
|
3502
|
+
projectContext: now?.projectContext ?? services.projectContext
|
|
3503
|
+
};
|
|
3504
|
+
};
|
|
3505
|
+
const checkpoints = new CheckpointManager({
|
|
3506
|
+
workDir: () => site().workDir,
|
|
3507
|
+
homeDir: config.homeDir
|
|
3508
|
+
});
|
|
3590
3509
|
const loopProvider = opts.goals ? withGoalPrompt(
|
|
3591
3510
|
scope.model.provider,
|
|
3592
3511
|
() => opts.goals.promptView(live.session?.sessionId ?? sessionId)
|
|
@@ -3594,59 +3513,18 @@ ${context}` : goal,
|
|
|
3594
3513
|
const bridge = AgentSession.createApprovalBridge();
|
|
3595
3514
|
const routed = { approve: bridge.onApprovalRequest, ask: bridge.onQuestionRequest };
|
|
3596
3515
|
opts.bridges.register(sessionId, routed);
|
|
3597
|
-
const live = {};
|
|
3598
3516
|
const agent = safeInit(
|
|
3599
3517
|
"AgentLoop",
|
|
3600
3518
|
() => new AgentLoop({
|
|
3601
|
-
// **这个会话自己那份门面**,不是 `opts.provider`(那是引导会话的)。
|
|
3602
|
-
// 拿错的表现是:第二个会话换完模型,界面上写着新的、引擎下一轮打的
|
|
3603
|
-
// 还是旧的 —— 判据在 `SessionModelScope.provider` 上
|
|
3604
3519
|
provider: loopProvider,
|
|
3605
|
-
// **给 provider 而不是 `exposed()` 的快照**:MCP server 发
|
|
3606
|
-
// list_changed / 断线重连 / login 解锁新工具时,正在跑的这个 loop
|
|
3607
|
-
// 下一轮就能看到新表(方案 15)。这里给的是**包了两层收窄的那个** ——
|
|
3608
|
-
// 命令那一层(方案 23)在 `opts.toolProvider` 里,角色那一层是槽
|
|
3609
|
-
// (方案 57 §3.2)。两层都是可变的,上面那条性质仍然成立。
|
|
3610
|
-
//
|
|
3611
|
-
// 收窄**不再按 `active` 分支**:槽在 `active` 缺席时是一层透明包装
|
|
3612
|
-
// (`roleScopedProvider(base, undefined, [])` 一个工具都不减),
|
|
3613
|
-
// 所以这一行的字节行为和改造前逐字相同。分支留着的话,身份那一半会在
|
|
3614
|
-
// 建 loop 那一刻定死「有没有角色」,而工具那一半是活的 —— 那正是
|
|
3615
|
-
// 方案 57 §1.3 那个劈叉,只是换了个更不容易看出来的位置
|
|
3616
3520
|
toolProvider: roleScope.provider,
|
|
3521
|
+
codeModeTools: opts.codeModeTools,
|
|
3617
3522
|
telemetry: opts.telemetry,
|
|
3618
|
-
|
|
3619
|
-
projectContext,
|
|
3620
|
-
// artifact 落在**这次实际生效的数据目录**下,而不是全局 `~/.epoch`
|
|
3621
|
-
// (方案 54 §二)。给 `config.homeDir` 而不是 `opts.homeDir`:
|
|
3622
|
-
// 不传时它是引擎探测出来的结果,只有它是真源
|
|
3523
|
+
site,
|
|
3623
3524
|
multimodal: { homeDir: config.homeDir },
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
// ⚠️ **读的是 `active`(这段会话默认那个专家)而不是槽 —— 逐条换专家
|
|
3627
|
-
// 不换轮次预算**,这是一个答案不是一笔欠账(方案 57 §3.2.1,2026-08-17)。
|
|
3628
|
-
// 判据一句话:身份和工具必须同作用域是因为它们**同时被模型读到**、
|
|
3629
|
-
// 劈叉了看不出来;而轮次预算是宿主侧的一个上界,它劈叉的表现是
|
|
3630
|
-
// `finish` 上那个 `max-turns`,屏幕上说得清。真要换只能走
|
|
3631
|
-
// `RunInput.maxTurns`,而那一格已经有调用方了(`--max-turns`),
|
|
3632
|
-
// 「谁压过谁」是另一次判断。全文在 `agent-role/scope.ts` 的 `current` 上
|
|
3525
|
+
artifactRetention: resolveArtifactRetention(config.artifacts),
|
|
3526
|
+
sandboxTerminal: config.sandbox?.terminal !== false,
|
|
3633
3527
|
maxTurns: active?.maxTurns ?? config.maxTurns,
|
|
3634
|
-
// 工具白名单**不传**:它已经被上面的 `roleScopedProvider` 消化掉了,
|
|
3635
|
-
// loop 里不该存在「工具表和角色不一致」这种状态。
|
|
3636
|
-
//
|
|
3637
|
-
// `scope: 'session'` 是这一轮加的那一格,而且**必须显式给** ——
|
|
3638
|
-
// 它缺省是 `'delegate'`(老调用方的 prompt 要逐字节不变),不给的话
|
|
3639
|
-
// 顶层会话会收到一句「由主智能体派来完成一个独立子任务」的假话,
|
|
3640
|
-
// 而这一条从 `--agent` 落地那天起就错着(判据见 `AgentRoleScope`)。
|
|
3641
|
-
//
|
|
3642
|
-
// `source` 纯展示,进不了 prompt —— 它是 `run-start` 事件上那枚来源
|
|
3643
|
-
// 徽标的唯一来路(决定 8 第一个时刻)。这一层是全仓唯一同时握着
|
|
3644
|
-
// 「完整 AgentRole」和「这个会话的 loop」的地方,不在这儿给就没人给得出
|
|
3645
|
-
//
|
|
3646
|
-
// **从槽里读,而且这一格不再按 `active` 分支**(方案 57 §3.2):
|
|
3647
|
-
// 工具那一半已经是活的了,身份这一半再钉死在建 loop 那一刻,就是
|
|
3648
|
-
// §1.3 那个劈叉。槽空着时函数回 `undefined`,loop 那边整个键缺席 ——
|
|
3649
|
-
// 和改造前 `active` 缺席时**逐字节等价**
|
|
3650
3528
|
role: () => {
|
|
3651
3529
|
const now = roleScope.current;
|
|
3652
3530
|
if (!now) return void 0;
|
|
@@ -3658,28 +3536,11 @@ ${context}` : goal,
|
|
|
3658
3536
|
};
|
|
3659
3537
|
},
|
|
3660
3538
|
contextLength: services.contextLength,
|
|
3661
|
-
// 已批准的计划(方案 35 PR-2 验收 8)。给函数不给快照 —— 计划是这一轮
|
|
3662
|
-
// 跑到一半才被批准的,快照恒为 null。
|
|
3663
|
-
//
|
|
3664
|
-
// ⚠️ 读的是**这个会话自己那份** plan 状态(2026-08-16):原来这儿是一个
|
|
3665
|
-
// 进程级闭包,于是会话 B 的 system prompt 附加段里挂的是会话 A 批准的
|
|
3666
|
-
// 那份纲 —— 模型照着一份不属于这段对话的计划干活,而屏幕上什么都看不出来
|
|
3667
3539
|
...planState ? { approvedPlan: () => planState.approvedPlan() } : {},
|
|
3668
|
-
// 宿主给了阻塞式确认就用它的(单会话宿主),否则走这个会话自己那座桥。
|
|
3669
|
-
// **不绕 `bridges.approve`**:那一层是给共享工具路由用的,而 `AgentLoop`
|
|
3670
|
-
// 本来就知道自己是谁 —— 多绕一圈只多一处能对错的地方
|
|
3671
3540
|
onApprovalRequest: opts.hostApproval ?? bridge.onApprovalRequest,
|
|
3672
|
-
// 只给**这个会话**那一批(2026-08-15)。会话 id 走 `live` 而不是入参,
|
|
3673
|
-
// 理由见它的声明
|
|
3674
3541
|
backgroundTasks: () => opts.backgroundTasks(live.session?.sessionId ?? sessionId),
|
|
3675
|
-
// 写类工具动手前留一份旧内容(方案 27)。开轮在 `AgentSession.run()`,
|
|
3676
|
-
// 循环自己不认识这个东西 —— 所以上面那条「同一个实例」是硬约束
|
|
3677
3542
|
checkpoints,
|
|
3678
3543
|
...services.memory ? { memory: services.memory } : {},
|
|
3679
|
-
// 压缩时把当前目标递给摘要模型(方案 52 PR-3 / 验收 3 后半句)。
|
|
3680
|
-
// **包的是同一个进程级实例**,不是新建一个 —— 冷却期、熔断计数、
|
|
3681
|
-
// 上一版摘要都在那份状态里,复制一份等于每个会话各有一套闸门。
|
|
3682
|
-
// 判据全文在 `goal/engine.ts`
|
|
3683
3544
|
...services.contextEngine ? {
|
|
3684
3545
|
contextEngine: opts.goals ? withGoalCompression(
|
|
3685
3546
|
services.contextEngine,
|
|
@@ -3687,11 +3548,9 @@ ${context}` : goal,
|
|
|
3687
3548
|
) : services.contextEngine
|
|
3688
3549
|
} : {},
|
|
3689
3550
|
...services.hookManager ? { hookManager: services.hookManager } : {},
|
|
3690
|
-
|
|
3551
|
+
skillSystem: services.skill ?? void 0,
|
|
3552
|
+
roleSkills: () => roleScope.current?.skills,
|
|
3691
3553
|
...services.skillLearner ? { skillLearner: services.skillLearner } : {},
|
|
3692
|
-
// **这个会话自己那一档**(2026-08-16)。规则表 / 审批缓存 / 审计流水
|
|
3693
|
-
// 照旧是同一份(core 的 `PermissionManager.scoped()` 共享它们),
|
|
3694
|
-
// 分家的只有「此刻是哪一档」
|
|
3695
3554
|
...scope.guard.permission ? { permissionManager: scope.guard.permission } : {},
|
|
3696
3555
|
...opts.budget
|
|
3697
3556
|
}),
|
|
@@ -3704,7 +3563,7 @@ ${context}` : goal,
|
|
|
3704
3563
|
const store = opts.sessions ? new SessionStore(
|
|
3705
3564
|
opts.sessions,
|
|
3706
3565
|
config.homeDir,
|
|
3707
|
-
projectContext.rootDir,
|
|
3566
|
+
() => site().projectContext.rootDir,
|
|
3708
3567
|
opts.sessionSource,
|
|
3709
3568
|
(id) => opts.workspaces.decisionOf(id)
|
|
3710
3569
|
) : void 0;
|
|
@@ -3714,15 +3573,9 @@ ${context}` : goal,
|
|
|
3714
3573
|
...store ? { store } : {},
|
|
3715
3574
|
history: [...history],
|
|
3716
3575
|
checkpoints,
|
|
3717
|
-
// 一轮开始时给目标 +1 并检查预算(方案 52 PR-2)。**接在轮次边界上,
|
|
3718
|
-
// 而 `loop.ts` 一行没改** —— 判据在方案 52 §四:一次 `run()` 恰好是一条
|
|
3719
|
-
// 被接纳的用户消息,而循环内部那个 `for (turn = 1..maxTurns)` 数的是模型
|
|
3720
|
-
// 请求次数。挂在这儿和 `checkpoints` 是同一个位置、同一条理由
|
|
3721
3576
|
...opts.goals ? { goals: opts.goals } : {},
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
// 「没换人」—— 而那正是这条留痕唯一会坏的方式(不报错、只是永远不出现)
|
|
3725
|
-
currentRole: () => roleScope.current?.name
|
|
3577
|
+
currentRole: () => roleScope.current?.name,
|
|
3578
|
+
currentWorkDir: () => site().workDir
|
|
3726
3579
|
});
|
|
3727
3580
|
bridge.attach(session);
|
|
3728
3581
|
live.session = session;
|
|
@@ -3734,38 +3587,17 @@ ${context}` : goal,
|
|
|
3734
3587
|
session,
|
|
3735
3588
|
agent,
|
|
3736
3589
|
model: scope.model.control,
|
|
3737
|
-
// 这个会话那个角色槽的宿主面(方案 57 §3.4)。**在这儿包而不是把 `roleScope`
|
|
3738
|
-
// 整个交出去**:`enter()` 收的是 `AgentRole` 对象,而服务端不许 import core
|
|
3739
|
-
// ——它手里只有网线上那个字符串。判据全文在 `role-scope.ts` 文件头
|
|
3740
3590
|
roles: createRoleControl(roleScope, opts.roles),
|
|
3741
|
-
/*
|
|
3742
|
-
* 会话级那一份身份(2026-08-18)。**取 `active` 而不是入参 `role`**:
|
|
3743
|
-
* 那个变量已经把「没带就退到 `--agent`」算过一次,而上面那句「全函数只算
|
|
3744
|
-
* 这一次」正是为了不让两处给出不同答案。判据全文在 {@link LiveSession.role}。
|
|
3745
|
-
*
|
|
3746
|
-
* 只带 `name` + `source` 两格(`ActiveAgentRole`),不把整个 `AgentRole`
|
|
3747
|
-
* 交出去:服务端不许 import core,而它要的就是这两个字段
|
|
3748
|
-
* —— 同 `roles` 那一格「包成两个动词」的判据。
|
|
3749
|
-
*/
|
|
3750
3591
|
role: toActiveRole(active),
|
|
3751
3592
|
provider: scope.model.provider,
|
|
3752
3593
|
permission: scope.guard.permission,
|
|
3753
3594
|
permissions: scope.guard.permissions,
|
|
3754
3595
|
plan: scope.guard.plan,
|
|
3755
|
-
// 会话 id 走 `session.sessionId` 而不是入参,判据同下面那个
|
|
3756
|
-
// `buildCheckpointControl`:`/resume` 会把它换掉
|
|
3757
3596
|
goals: opts.goals ? buildGoalControl(opts.goals, () => session.sessionId) : null,
|
|
3758
|
-
// **同一个 `checkpoints` 实例**再包一层交出去,不是新建一个管理器:
|
|
3759
|
-
// 上面那条「`AgentSession` 和它的 `AgentLoop` 必须共用同一个实例」
|
|
3760
|
-
// 对宿主这一侧同样成立 —— 另建一个的表现是回退列表读得出、
|
|
3761
|
-
// 而「这一轮开过没有」是另一本账。
|
|
3762
|
-
//
|
|
3763
|
-
// 会话 id 走 `session.sessionId` 而不是上面那个入参:`/resume` 会把它换掉
|
|
3764
|
-
// (`rekey` 只换工厂表里的键,换不了一个拍过快照的闭包)
|
|
3765
3597
|
checkpoints: buildCheckpointControl(checkpoints, session, () => session.sessionId),
|
|
3766
|
-
workspace
|
|
3767
|
-
|
|
3768
|
-
|
|
3598
|
+
get workspace() {
|
|
3599
|
+
return opts.workspaces.of(live.session?.sessionId ?? sessionId);
|
|
3600
|
+
},
|
|
3769
3601
|
bridge: routed
|
|
3770
3602
|
};
|
|
3771
3603
|
}
|
|
@@ -3823,7 +3655,6 @@ var init_file = __esm({
|
|
|
3823
3655
|
opts;
|
|
3824
3656
|
now;
|
|
3825
3657
|
ready = false;
|
|
3826
|
-
/** 自增的 span id。同一个进程内唯一就够 —— 这文件本来就是单机看的 */
|
|
3827
3658
|
seq = 0;
|
|
3828
3659
|
startSpan(name, attrs, parent) {
|
|
3829
3660
|
const startedAt = this.now();
|
|
@@ -3957,8 +3788,6 @@ async function tryCreateOtelTelemetry(endpoint) {
|
|
|
3957
3788
|
};
|
|
3958
3789
|
return {
|
|
3959
3790
|
telemetry,
|
|
3960
|
-
// 退出时必须 flush(批量导出器手上压着最后一轮的 span),但**必须有上限** ——
|
|
3961
|
-
// 见 SHUTDOWN_BUDGET_MS
|
|
3962
3791
|
shutdown: async () => {
|
|
3963
3792
|
await withTimeout(
|
|
3964
3793
|
Promise.allSettled([tracerProvider.shutdown(), meterProvider.shutdown()]).then(() => {
|
|
@@ -3982,7 +3811,7 @@ async function buildTelemetry(input) {
|
|
|
3982
3811
|
const noop = { telemetry: NOOP_TELEMETRY, dispose: () => {
|
|
3983
3812
|
} };
|
|
3984
3813
|
if (!config?.enabled) {
|
|
3985
|
-
diags.skipped("Telemetry", "
|
|
3814
|
+
diags.skipped("Telemetry", t("boot.telemetry_off"));
|
|
3986
3815
|
return noop;
|
|
3987
3816
|
}
|
|
3988
3817
|
const target = config.target ?? "file";
|
|
@@ -3995,7 +3824,7 @@ async function buildTelemetry(input) {
|
|
|
3995
3824
|
}
|
|
3996
3825
|
diags.warn(
|
|
3997
3826
|
"Telemetry",
|
|
3998
|
-
|
|
3827
|
+
(lang) => t("boot.telemetry_otlp_missing", { command: OTLP_INSTALL_COMMAND }, lang),
|
|
3999
3828
|
"otlp-deps-missing"
|
|
4000
3829
|
);
|
|
4001
3830
|
}
|
|
@@ -4009,7 +3838,6 @@ async function buildTelemetry(input) {
|
|
|
4009
3838
|
return {
|
|
4010
3839
|
telemetry: new FileTelemetry({
|
|
4011
3840
|
path,
|
|
4012
|
-
// 写不进去要说出来,但只说一次 —— 每条记录报一次会把诊断刷爆
|
|
4013
3841
|
onError: onceWarn(diags, path)
|
|
4014
3842
|
}),
|
|
4015
3843
|
dispose: () => {
|
|
@@ -4023,7 +3851,11 @@ function onceWarn(diags, path) {
|
|
|
4023
3851
|
reported = true;
|
|
4024
3852
|
diags.warn(
|
|
4025
3853
|
"Telemetry",
|
|
4026
|
-
|
|
3854
|
+
(lang) => t(
|
|
3855
|
+
"boot.telemetry_write_failed",
|
|
3856
|
+
{ reason: err instanceof Error ? err.message : String(err), path },
|
|
3857
|
+
lang
|
|
3858
|
+
),
|
|
4027
3859
|
"telemetry-write-failed"
|
|
4028
3860
|
);
|
|
4029
3861
|
};
|
|
@@ -4050,16 +3882,18 @@ function consoleTelemetry() {
|
|
|
4050
3882
|
histogram: (name, value, attrs) => line({ t: "histogram", name, v: value, attrs })
|
|
4051
3883
|
};
|
|
4052
3884
|
}
|
|
3885
|
+
var OTLP_INSTALL_COMMAND;
|
|
4053
3886
|
var init_telemetry = __esm({
|
|
4054
3887
|
"src/telemetry/index.ts"() {
|
|
4055
3888
|
init_file();
|
|
4056
3889
|
init_otel();
|
|
4057
3890
|
init_file();
|
|
4058
3891
|
init_otel();
|
|
3892
|
+
OTLP_INSTALL_COMMAND = "pnpm add @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/sdk-metrics @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http @opentelemetry/resources";
|
|
4059
3893
|
}
|
|
4060
3894
|
});
|
|
4061
3895
|
function missing(tool) {
|
|
4062
|
-
return { output: `${tool}
|
|
3896
|
+
return { output: t("boot.tool_unavailable", { tool: `${tool} ` }), ok: false };
|
|
4063
3897
|
}
|
|
4064
3898
|
function toResult(result) {
|
|
4065
3899
|
const output = typeof result.output === "string" ? result.output : JSON.stringify(result.output, null, 2);
|
|
@@ -4078,7 +3912,11 @@ function createUserActions(deps) {
|
|
|
4078
3912
|
const type = mapOperationType(toolName);
|
|
4079
3913
|
const decision = pm.check({ type, detail, workDir: deps.workDir, target, toolName });
|
|
4080
3914
|
if (decision.allowed) return null;
|
|
4081
|
-
if (!decision.requireApproval)
|
|
3915
|
+
if (!decision.requireApproval) {
|
|
3916
|
+
return t("boot.blocked_by_permission", {
|
|
3917
|
+
reason: decision.reason || t("boot.needs_authorization")
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
4082
3920
|
const outcome = await approve({
|
|
4083
3921
|
toolName,
|
|
4084
3922
|
type,
|
|
@@ -4087,7 +3925,7 @@ function createUserActions(deps) {
|
|
|
4087
3925
|
...decision.reason ? { reason: decision.reason } : {}
|
|
4088
3926
|
});
|
|
4089
3927
|
pm.auditUserDecision?.({ type, target, toolName }, outcome);
|
|
4090
|
-
if (outcome === "deny") return "
|
|
3928
|
+
if (outcome === "deny") return t("boot.cancelled");
|
|
4091
3929
|
pm.recordApproval(toolName, target, outcome);
|
|
4092
3930
|
return null;
|
|
4093
3931
|
};
|
|
@@ -4128,6 +3966,139 @@ var init_user_shell = __esm({
|
|
|
4128
3966
|
"src/user-shell.ts"() {
|
|
4129
3967
|
}
|
|
4130
3968
|
});
|
|
3969
|
+
function utilityBudgetKey(input) {
|
|
3970
|
+
const owner = input.sessionId?.trim();
|
|
3971
|
+
if (owner) return owner;
|
|
3972
|
+
const label = input.label !== void 0 && LABEL_SHAPE.test(input.label) ? input.label : ANONYMOUS;
|
|
3973
|
+
return `${BUCKET_PREFIX}${label}`;
|
|
3974
|
+
}
|
|
3975
|
+
function buildUtilityControl(opts) {
|
|
3976
|
+
const { provider, store, pricingOf } = opts;
|
|
3977
|
+
const record = (input, out) => {
|
|
3978
|
+
if (!store || !out.usage) return;
|
|
3979
|
+
const pricing = pricingOf?.(out.model);
|
|
3980
|
+
new BudgetGuard({ store, sessionId: utilityBudgetKey(input) }).add(out.usage, {
|
|
3981
|
+
model: out.model,
|
|
3982
|
+
...pricing ? { pricing } : {}
|
|
3983
|
+
});
|
|
3984
|
+
};
|
|
3985
|
+
return {
|
|
3986
|
+
async generate(input) {
|
|
3987
|
+
if (input.signal?.aborted === true) throw new Error(t("utility.aborted"));
|
|
3988
|
+
const wanted = input.model ?? refOf(provider.getUtilityInfo());
|
|
3989
|
+
const out = await provider.generate({
|
|
3990
|
+
messages: input.messages,
|
|
3991
|
+
...input.system !== void 0 ? { system: input.system } : {},
|
|
3992
|
+
...input.maxTokens !== void 0 ? { maxTokens: input.maxTokens } : {},
|
|
3993
|
+
...wanted.provider ? { provider: wanted.provider } : {},
|
|
3994
|
+
model: wanted.model,
|
|
3995
|
+
...input.responseFormat ? { responseFormat: input.responseFormat } : {},
|
|
3996
|
+
...input.providerOptions ? { providerOptions: input.providerOptions } : {}
|
|
3997
|
+
});
|
|
3998
|
+
record(input, out);
|
|
3999
|
+
return {
|
|
4000
|
+
text: out.text,
|
|
4001
|
+
...out.usage ? { usage: out.usage } : {},
|
|
4002
|
+
model: out.model,
|
|
4003
|
+
provider: out.provider,
|
|
4004
|
+
...out.responseFormatApplied === void 0 ? {} : { responseFormatApplied: out.responseFormatApplied },
|
|
4005
|
+
...out.warnings ? { warnings: out.warnings } : {}
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
};
|
|
4009
|
+
}
|
|
4010
|
+
function refOf(info) {
|
|
4011
|
+
return { provider: info.selection.provider, model: info.selection.model };
|
|
4012
|
+
}
|
|
4013
|
+
var BUCKET_PREFIX, ANONYMOUS, LABEL_SHAPE;
|
|
4014
|
+
var init_utility = __esm({
|
|
4015
|
+
"src/utility.ts"() {
|
|
4016
|
+
BUCKET_PREFIX = "utility:";
|
|
4017
|
+
ANONYMOUS = "anonymous";
|
|
4018
|
+
LABEL_SHAPE = /^[a-z0-9_-]{1,32}$/;
|
|
4019
|
+
}
|
|
4020
|
+
});
|
|
4021
|
+
function withinWorkspace(resolveInWorkspace2, root, rel) {
|
|
4022
|
+
return resolveInWorkspace2(root, rel) !== null;
|
|
4023
|
+
}
|
|
4024
|
+
function buildWorkspaceFilesControl(deps, permission) {
|
|
4025
|
+
let cache = null;
|
|
4026
|
+
const now = () => Date.now();
|
|
4027
|
+
function computeList(root) {
|
|
4028
|
+
const { files, truncated } = deps.listFiles(root);
|
|
4029
|
+
return { files, truncated, at: now(), root };
|
|
4030
|
+
}
|
|
4031
|
+
function listFor(root) {
|
|
4032
|
+
const t23 = now();
|
|
4033
|
+
if (!cache || cache.root !== root) cache = computeList(root);
|
|
4034
|
+
else if (t23 - cache.at > LIST_TTL_MS) cache = computeList(root);
|
|
4035
|
+
return cache;
|
|
4036
|
+
}
|
|
4037
|
+
return {
|
|
4038
|
+
candidates(query, limit, root) {
|
|
4039
|
+
const q = query.trim().toLowerCase();
|
|
4040
|
+
let base;
|
|
4041
|
+
try {
|
|
4042
|
+
base = listFor(root);
|
|
4043
|
+
} catch {
|
|
4044
|
+
return null;
|
|
4045
|
+
}
|
|
4046
|
+
const mentionable = [];
|
|
4047
|
+
let unmentionable = 0;
|
|
4048
|
+
for (const rel of base.files) {
|
|
4049
|
+
if (!withinWorkspace(deps.resolveInWorkspace, root, rel)) continue;
|
|
4050
|
+
const hit = q.length === 0 || rel.toLowerCase().includes(q);
|
|
4051
|
+
if (!hit) continue;
|
|
4052
|
+
if (isMentionableFilePath(rel)) {
|
|
4053
|
+
mentionable.push({ rel, depth: rel.split("/").length });
|
|
4054
|
+
} else {
|
|
4055
|
+
unmentionable += 1;
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
mentionable.sort((a, b) => a.depth - b.depth || (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
4059
|
+
return {
|
|
4060
|
+
candidates: mentionable.slice(0, limit).map((row) => row.rel),
|
|
4061
|
+
unmentionable,
|
|
4062
|
+
truncated: base.truncated
|
|
4063
|
+
};
|
|
4064
|
+
},
|
|
4065
|
+
readFile(path, root) {
|
|
4066
|
+
const full = deps.resolveInWorkspace(root, path);
|
|
4067
|
+
if (!full) return { ok: false, reason: "denied" };
|
|
4068
|
+
const decision = permission?.check({
|
|
4069
|
+
type: "file_read",
|
|
4070
|
+
detail: path,
|
|
4071
|
+
workDir: root,
|
|
4072
|
+
target: full,
|
|
4073
|
+
toolName: "file_read"
|
|
4074
|
+
});
|
|
4075
|
+
if (permission && !decision?.allowed) return { ok: false, reason: "denied" };
|
|
4076
|
+
let stat;
|
|
4077
|
+
try {
|
|
4078
|
+
stat = statSync(full);
|
|
4079
|
+
if (!stat.isFile()) return { ok: false, reason: "unreadable" };
|
|
4080
|
+
} catch {
|
|
4081
|
+
return { ok: false, reason: "unreadable" };
|
|
4082
|
+
}
|
|
4083
|
+
let raw;
|
|
4084
|
+
try {
|
|
4085
|
+
raw = readFileSync(full);
|
|
4086
|
+
} catch {
|
|
4087
|
+
return { ok: false, reason: "unreadable" };
|
|
4088
|
+
}
|
|
4089
|
+
if (raw.subarray(0, 8192).includes(0)) {
|
|
4090
|
+
return { ok: false, reason: "binary", bytes: stat.size };
|
|
4091
|
+
}
|
|
4092
|
+
return { ok: true, text: raw.toString("utf8"), bytes: stat.size };
|
|
4093
|
+
}
|
|
4094
|
+
};
|
|
4095
|
+
}
|
|
4096
|
+
var LIST_TTL_MS;
|
|
4097
|
+
var init_workspace_files = __esm({
|
|
4098
|
+
"src/workspace-files.ts"() {
|
|
4099
|
+
LIST_TTL_MS = 6e4;
|
|
4100
|
+
}
|
|
4101
|
+
});
|
|
4131
4102
|
function splitDirList(value) {
|
|
4132
4103
|
if (!value) return [];
|
|
4133
4104
|
return value.split(delimiter).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -4137,20 +4108,23 @@ function wireWorkspace(input) {
|
|
|
4137
4108
|
input.permission?.setExtraRoots(workspace.extra);
|
|
4138
4109
|
if (input.addDir.length === 0) return workspace;
|
|
4139
4110
|
for (const issue of issues) {
|
|
4140
|
-
if (issue.kind === "redundant") input.diags.skipped("
|
|
4141
|
-
else input.diags.warn("
|
|
4111
|
+
if (issue.kind === "redundant") input.diags.skipped("ExtraDirs", issue.detail);
|
|
4112
|
+
else input.diags.warn("ExtraDirs", issue.detail, `add-dir-${issue.kind}`);
|
|
4142
4113
|
}
|
|
4143
4114
|
if (workspace.extra.length > 0) {
|
|
4144
4115
|
input.diags.ok(
|
|
4145
4116
|
WORKSPACE_MODULE,
|
|
4146
|
-
|
|
4117
|
+
t("boot.workspace_extra", {
|
|
4118
|
+
count: workspace.extra.length,
|
|
4119
|
+
dirs: workspace.extra.join(", ")
|
|
4120
|
+
})
|
|
4147
4121
|
);
|
|
4148
4122
|
}
|
|
4149
4123
|
const unloaded = unloadedInstructionFiles(workspace);
|
|
4150
4124
|
if (unloaded.length > 0) {
|
|
4151
4125
|
input.diags.skipped(
|
|
4152
|
-
"
|
|
4153
|
-
|
|
4126
|
+
"ExtraDirInstructions",
|
|
4127
|
+
t("boot.workspace_extra_instructions", { paths: unloaded.join(", ") })
|
|
4154
4128
|
);
|
|
4155
4129
|
}
|
|
4156
4130
|
return workspace;
|
|
@@ -4161,10 +4135,6 @@ function workspaceDecisionLog(sessions, diags) {
|
|
|
4161
4135
|
try {
|
|
4162
4136
|
sessions.updateMeta(sessionId, {
|
|
4163
4137
|
workspaceState: decision.state,
|
|
4164
|
-
// `none` 那一档**不带路径**。写 `undefined` 在 `updateSessionMeta()` 里
|
|
4165
|
-
// 是「这一列不进 SET」(那儿第一行就 `if (v === undefined) continue`),
|
|
4166
|
-
// 不是「清空它」—— 而 `bound → none` 这条路被 server 那道 409 挡着,
|
|
4167
|
-
// 所以「留着一个陈旧的 root」在今天不可达
|
|
4168
4138
|
...decision.state === "bound" ? { workspaceRoot: decision.root } : {}
|
|
4169
4139
|
});
|
|
4170
4140
|
} catch (err) {
|
|
@@ -4210,51 +4180,10 @@ function inspectRoot(dir, lang) {
|
|
|
4210
4180
|
var WORKSPACE_MODULE, SessionWorkspaces;
|
|
4211
4181
|
var init_workspace = __esm({
|
|
4212
4182
|
"src/workspace.ts"() {
|
|
4213
|
-
WORKSPACE_MODULE = "
|
|
4183
|
+
WORKSPACE_MODULE = "Workspace";
|
|
4214
4184
|
SessionWorkspaces = class {
|
|
4215
|
-
/**
|
|
4216
|
-
* 谁绑在哪儿。**一张内存 Map,进程退出就没了 —— 这一条 2026-08-19 之后仍然成立。**
|
|
4217
|
-
*
|
|
4218
|
-
* 变的是它旁边多了一份 {@link WorkspaceDecisionLog}:绑成功的那一刻**另外**
|
|
4219
|
-
* 往会话库记一笔「用户当初选了哪儿」。两者不是一份数据的两个副本,时态不同 ——
|
|
4220
|
-
* 这张表答的是「它**现在**绑在哪儿」(因此 {@link bind} 那道闸门:路径校验 +
|
|
4221
|
-
* 信任判定),落盘那份答的是「用户**当初**决定了什么」(只读的事实,不带闸门,
|
|
4222
|
-
* 因为决定本身不需要重新判定)。
|
|
4223
|
-
*
|
|
4224
|
-
* 于是原来那两条反对理由,一条仍然拦着、一条被那个时态差绕开了:
|
|
4225
|
-
*
|
|
4226
|
-
* 1. **仍然成立**:落盘成一条**自动生效**的绑定,会和 [build.ts](./build.ts) 里
|
|
4227
|
-
* `/resume` 那一行 {@link seed} 顶上 —— 那儿的规矩是「接回来的会话跑在
|
|
4228
|
-
* **这个进程**的工作区里」。侧栏画着 A、引擎跑在 B 就是这么来的。所以装配时
|
|
4229
|
-
* **没有任何东西读那份记录去 `bind()`**;绑定发生在用户点开旧会话、敲下第一句
|
|
4230
|
-
* 话那一刻(`SessionFactory.revive()`),走的是这个类完整的 {@link bind};
|
|
4231
|
-
* 2. **不再成立**:「只读的事实不属于这张表」是对的,但结论错了 —— 它属于
|
|
4232
|
-
* **会话库那一行**,而不是「所以谁都别记」。不记的代价是重启之后一段会话
|
|
4233
|
-
* 说不出自己在哪儿干过活,判据全文(含哪条过期、哪条原样成立)在 web 的
|
|
4234
|
-
* `shell/session-list.tsx` 文件头。
|
|
4235
|
-
*/
|
|
4236
4185
|
bindings = /* @__PURE__ */ new Map();
|
|
4237
|
-
/**
|
|
4238
|
-
* 「建出来了,但还没选地盘」的那几个会话(方案 55 PR-1,2026-08-17)。
|
|
4239
|
-
*
|
|
4240
|
-
* **一个 `Set<sessionId>`,不是第二张绑定表**:这一档的全部内容就是「它在等
|
|
4241
|
-
* 一次选择」,没有路径、没有信任判定、没有 `ProjectContext`。用一张 Map 存的话
|
|
4242
|
-
* 那些格子只能是 null,而一个到处是 null 的绑定会被下一个人当成「绑了个空的」。
|
|
4243
|
-
*
|
|
4244
|
-
* 出集合有且只有两条路:{@link bind} 成功(转成 `bound`)、
|
|
4245
|
-
* 以及 {@link release}(会话没了)。「明确选不使用工作区」那一下在宿主那一层
|
|
4246
|
-
* 也走 `release` —— 判据见它。
|
|
4247
|
-
*/
|
|
4248
4186
|
deferred = /* @__PURE__ */ new Set();
|
|
4249
|
-
/**
|
|
4250
|
-
* 明确选过「不使用工作区」的那几个(会话库 v7,2026-08-19)。
|
|
4251
|
-
*
|
|
4252
|
-
* **{@link stateOf} 不看它**,那一档的语义一个字没变。这个集合存在只为回答
|
|
4253
|
-
* {@link decisionOf} 的第三种答案 `null`:`stateOf()` 把「明确不使用」和
|
|
4254
|
-
* 「这个进程从没听说过这个 id」都报成 `none`(那条 ⚠️ 就写在它的文档上),
|
|
4255
|
-
* 而落盘的时候这两者天差地别 —— 前者是一次决定,后者写下去就是**替用户
|
|
4256
|
-
* 编了一个他没做过的决定**。
|
|
4257
|
-
*/
|
|
4258
4187
|
noWorkspace = /* @__PURE__ */ new Set();
|
|
4259
4188
|
gate;
|
|
4260
4189
|
recent;
|
|
@@ -4266,51 +4195,17 @@ var init_workspace = __esm({
|
|
|
4266
4195
|
this.decisions = opts.decisions ?? null;
|
|
4267
4196
|
this.now = opts.now ?? Date.now;
|
|
4268
4197
|
}
|
|
4269
|
-
/** 这个会话绑在哪儿。没绑过 `null` —— 「不使用工作区」是真的一档,不是缺省值 */
|
|
4270
4198
|
of(sessionId) {
|
|
4271
4199
|
return this.bindings.get(sessionId) ?? null;
|
|
4272
4200
|
}
|
|
4273
|
-
/**
|
|
4274
|
-
* 记一笔「建出来了、还没选地盘」。判据全文在 {@link WorkspaceControl.defer}。
|
|
4275
|
-
*
|
|
4276
|
-
* **已经绑上的会话调它是空操作**,不是把它退回未选:那等于绕过决定 18 的
|
|
4277
|
-
* 「绑定后不给改」—— 一个宿主只要先 `defer()` 再 `bind()`,就能给一段跑过
|
|
4278
|
-
* 一整轮活的会话换地盘,而 `bind()` 那道 `locked` 闸拦的正是这件事。
|
|
4279
|
-
*/
|
|
4280
4201
|
defer(sessionId) {
|
|
4281
4202
|
if (this.bindings.has(sessionId)) return;
|
|
4282
4203
|
this.deferred.add(sessionId);
|
|
4283
4204
|
}
|
|
4284
|
-
/** 这个会话是哪一档。判据见 {@link WorkspaceBindingState} */
|
|
4285
4205
|
stateOf(sessionId) {
|
|
4286
4206
|
if (this.bindings.has(sessionId)) return "bound";
|
|
4287
4207
|
return this.deferred.has(sessionId) ? "unbound" : "none";
|
|
4288
4208
|
}
|
|
4289
|
-
/**
|
|
4290
|
-
* 装配时把**引导会话**的绑定记进来。
|
|
4291
|
-
*
|
|
4292
|
-
* 和 {@link bind} 分开是因为这一份已经算好了:`buildRuntime()` 手里的
|
|
4293
|
-
* `workspace`(含 `--add-dir`)、`services.trust`、`services.projectContext`
|
|
4294
|
-
* 就是引擎**真在用**的那三样。走 bind() 会重新 `discoverProject()`(要跑 git)
|
|
4295
|
-
* 算出第二份 —— 那第二份哪怕只差一个字节,界面上写的就不是引擎真在用的那个。
|
|
4296
|
-
*
|
|
4297
|
-
* `unloadedInstructions` **由这里补**,不要调用方给:它是 `workspace` 的纯函数,
|
|
4298
|
-
* 而让两个 `seed()` 调用点各算一遍的失败形态是其中一个漏了 ——
|
|
4299
|
-
* 结果是「resume 之后那条『额外目录的 CLAUDE.md 没加载』的提示凭空消失」。
|
|
4300
|
-
*
|
|
4301
|
-
* @param opts.decision 顺带把这一份**落盘**成用户的工作区决定(会话库 v7)。
|
|
4302
|
-
* **两个调用点一个给一个不给,这正是这个参数存在的全部理由**:
|
|
4303
|
-
*
|
|
4304
|
-
* - `buildRuntime()` 里引导会话那一句 —— **给**。用户在哪个目录里敲的
|
|
4305
|
-
* `epoch` / `epoch web`,那就是他为这段会话选的地盘,和在 picker 里点一下
|
|
4306
|
-
* 没有区别。不给的话「直接 `epoch web` 聊两句」这条最短路径重启后一律
|
|
4307
|
-
* 掉进「早先的会话」,而那正是这一轮要修的;
|
|
4308
|
-
* - `/resume` 那一句(`build.ts` 的 `SessionControl.resume` 回调)——
|
|
4309
|
-
* **不给**。那儿的规矩是「接回来的会话跑在**这个进程**的工作区里」,
|
|
4310
|
-
* 它是一次**临时接管**,不是那段会话的主人改了主意。落盘它的后果是:
|
|
4311
|
-
* 随手在 `/tmp` 里 `epoch --resume` 一段三周前的会话,就把它在库里的
|
|
4312
|
-
* 地盘永久改成了 `/tmp`。
|
|
4313
|
-
*/
|
|
4314
4209
|
seed(sessionId, bound, opts) {
|
|
4315
4210
|
this.bindings.set(sessionId, {
|
|
4316
4211
|
...bound,
|
|
@@ -4322,17 +4217,6 @@ var init_workspace = __esm({
|
|
|
4322
4217
|
this.decisions?.record(sessionId, { state: "bound", root: bound.workspace.root });
|
|
4323
4218
|
}
|
|
4324
4219
|
}
|
|
4325
|
-
/**
|
|
4326
|
-
* 把一个目录绑到会话上。
|
|
4327
|
-
*
|
|
4328
|
-
* 顺序是**先校验路径、再看有没有被锁**:一个写错的路径不管会话锁没锁都是写错的,
|
|
4329
|
-
* 而反过来的话「路径不存在」这条永远被 `locked` 盖住 —— 于是调用方收到的是
|
|
4330
|
-
* 「不能改」,而他真正的问题是「这个目录我打错了」。
|
|
4331
|
-
*
|
|
4332
|
-
* **只认绝对路径。** 相对路径要么相对服务进程的 cwd、要么相对用户以为的某个
|
|
4333
|
-
* 目录,两者不一致时没有任何迹象;而这条路的入口是网线(浏览器传字符串进来),
|
|
4334
|
-
* 那儿压根没有「当前目录」这个概念。
|
|
4335
|
-
*/
|
|
4336
4220
|
bind(sessionId, dir, lang) {
|
|
4337
4221
|
const invalid = inspectRoot(dir, lang);
|
|
4338
4222
|
if (invalid) return invalid;
|
|
@@ -4358,75 +4242,28 @@ var init_workspace = __esm({
|
|
|
4358
4242
|
this.decisions?.record(sessionId, { state: "bound", root });
|
|
4359
4243
|
return { ok: true, bound, changed: true };
|
|
4360
4244
|
}
|
|
4361
|
-
/** 记一笔「明确不使用工作区」。判据全文在 {@link WorkspaceControl.decideNone} */
|
|
4362
4245
|
decideNone(sessionId) {
|
|
4363
4246
|
if (this.bindings.has(sessionId)) return;
|
|
4364
4247
|
this.deferred.delete(sessionId);
|
|
4365
4248
|
this.noWorkspace.add(sessionId);
|
|
4366
4249
|
this.decisions?.record(sessionId, { state: "none" });
|
|
4367
4250
|
}
|
|
4368
|
-
/**
|
|
4369
|
-
* 用户为这个会话做过的工作区决定;**没做过是 `null`**。
|
|
4370
|
-
*
|
|
4371
|
-
* 这是 `SessionStore.ensure()` 建行那一刻要问的那一句(判据在
|
|
4372
|
-
* {@link WorkspaceDecisionLog.record} 的「实现方不许建行」那一节)。
|
|
4373
|
-
*
|
|
4374
|
-
* ⚠️ 和 {@link stateOf} 的差别只有一处,但那一处是全部意义所在:
|
|
4375
|
-
* `stateOf()` 把「明确不使用」和「没听说过这个 id」都答成 `none`,
|
|
4376
|
-
* 这里前者是 `{state:'none'}`、后者是 `null`。
|
|
4377
|
-
*/
|
|
4378
4251
|
decisionOf(sessionId) {
|
|
4379
4252
|
const bound = this.bindings.get(sessionId);
|
|
4380
4253
|
if (bound) return { state: "bound", root: bound.workspace.root };
|
|
4381
4254
|
return this.noWorkspace.has(sessionId) ? { state: "none" } : null;
|
|
4382
4255
|
}
|
|
4383
|
-
/**
|
|
4384
|
-
* 解绑(会话被删掉时)。
|
|
4385
|
-
*
|
|
4386
|
-
* **不动「已知工作区」清单**:那份记的是「我最近在哪几个目录里干过活」,
|
|
4387
|
-
* 删掉一段会话不代表那件事没发生过。
|
|
4388
|
-
*
|
|
4389
|
-
* ⚠️ 2026-08-17 起它也把 {@link defer} 那一笔清掉,于是「删掉一段还没选地盘的
|
|
4390
|
-
* 会话」不会在集合里留一个永远等不到选择的 id(方案 55 验收第 8 条)。
|
|
4391
|
-
*
|
|
4392
|
-
* ⚠️ **「明确选不使用工作区」那一下 2026-08-19 起不再走这里**,改走
|
|
4393
|
-
* {@link decideNone}。原来两者在内存里等价(摘掉 `defer` 那一笔之后
|
|
4394
|
-
* {@link stateOf} 就是 `none`),但落盘之后不再等价:这个方法**同时**是
|
|
4395
|
-
* 建会话失败的回滚路径和删会话的路径,在它里面记一次决定,等于让一段建到
|
|
4396
|
-
* 一半失败的会话在库里留下「用户选了不使用工作区」。判据全文在 `decideNone` 上。
|
|
4397
|
-
*
|
|
4398
|
-
* **它照旧把 `noWorkspace` 那一笔也摘掉** —— 会话都没了,那个决定连同它一起走。
|
|
4399
|
-
* ⚠️ 摘的只是内存这一份:库里那一行由 `DELETE /api/sessions/:id` 整行删掉,
|
|
4400
|
-
* 不是在这儿改字段。
|
|
4401
|
-
*/
|
|
4402
4256
|
release(sessionId) {
|
|
4403
4257
|
this.bindings.delete(sessionId);
|
|
4404
4258
|
this.deferred.delete(sessionId);
|
|
4405
4259
|
this.noWorkspace.delete(sessionId);
|
|
4406
4260
|
}
|
|
4407
|
-
/** 本机「已知工作区」清单,最近使用的在前 */
|
|
4408
4261
|
known() {
|
|
4409
4262
|
return this.recent.list();
|
|
4410
4263
|
}
|
|
4411
|
-
/**
|
|
4412
|
-
* 一个目录的信任判定。
|
|
4413
|
-
*
|
|
4414
|
-
* 和 {@link resolveBinding} 一样先归一到**项目根**再判 —— 两处分叉的表现是
|
|
4415
|
-
* 「清单里说未信任、绑上去之后指令文件却加载了」。
|
|
4416
|
-
*
|
|
4417
|
-
* 只查不写:`decide()` 是纯读(`TrustManager.check` + 降级规则),
|
|
4418
|
-
* 不往清单里记账,也不建绑定。
|
|
4419
|
-
*/
|
|
4420
4264
|
trustOf(dir) {
|
|
4421
4265
|
return this.gate.decide(resolveProjectRoot(dir));
|
|
4422
4266
|
}
|
|
4423
|
-
/**
|
|
4424
|
-
* 一个目录 → 完整的绑定。
|
|
4425
|
-
*
|
|
4426
|
-
* 信任按**项目根**判而不是按传进来那个目录判,和 `discoverProject()` 内部
|
|
4427
|
-
* 保持一致(在仓库子目录里绑定会继承仓库根的信任决定)。两处判据分叉的表现是
|
|
4428
|
-
* 「界面说未信任、指令文件却加载了」,或者反过来。
|
|
4429
|
-
*/
|
|
4430
4267
|
resolveBinding(root) {
|
|
4431
4268
|
const trust = this.gate.decide(resolveProjectRoot(root));
|
|
4432
4269
|
const { workspace } = resolveWorkspace(root);
|
|
@@ -4434,26 +4271,25 @@ var init_workspace = __esm({
|
|
|
4434
4271
|
workspace,
|
|
4435
4272
|
trust,
|
|
4436
4273
|
projectContext: discoverProject(root, trust),
|
|
4437
|
-
// 照旧探一遍而不是写死 `[]`:今天没有额外根所以它必然是空的,
|
|
4438
|
-
// 但「为什么是空的」这件事不该由一个字面量来记住
|
|
4439
4274
|
unloadedInstructions: unloadedInstructionFiles(workspace)
|
|
4440
4275
|
};
|
|
4441
4276
|
}
|
|
4442
4277
|
};
|
|
4443
4278
|
}
|
|
4444
4279
|
});
|
|
4445
|
-
|
|
4446
4280
|
// src/build.ts
|
|
4447
4281
|
var build_exports = {};
|
|
4448
4282
|
__export(build_exports, {
|
|
4449
4283
|
AgentRoleError: () => AgentRoleError,
|
|
4284
|
+
buildGoalCatalog: () => buildGoalCatalog,
|
|
4450
4285
|
buildRuntime: () => buildRuntime,
|
|
4451
4286
|
ensureSecretsReady: () => ensureSecretsReady
|
|
4452
4287
|
});
|
|
4453
4288
|
async function buildRuntime(opts = {}) {
|
|
4454
4289
|
const opened = { cleanups: null };
|
|
4290
|
+
const processes = createProcessTable();
|
|
4455
4291
|
try {
|
|
4456
|
-
return await assembleRuntime(opts, opened);
|
|
4292
|
+
return await assembleRuntime(opts, opened, processes);
|
|
4457
4293
|
} catch (err) {
|
|
4458
4294
|
for (const fn of [...opened.cleanups ?? []].reverse()) {
|
|
4459
4295
|
try {
|
|
@@ -4462,16 +4298,17 @@ async function buildRuntime(opts = {}) {
|
|
|
4462
4298
|
}
|
|
4463
4299
|
}
|
|
4464
4300
|
try {
|
|
4465
|
-
await
|
|
4301
|
+
await processes.release();
|
|
4466
4302
|
} catch {
|
|
4467
4303
|
}
|
|
4468
4304
|
throw err;
|
|
4469
4305
|
}
|
|
4470
4306
|
}
|
|
4471
|
-
async function assembleRuntime(opts, opened) {
|
|
4307
|
+
async function assembleRuntime(opts, opened, processes) {
|
|
4472
4308
|
const startedAt = Date.now();
|
|
4473
4309
|
const workDir = opts.workDir ?? process.cwd();
|
|
4474
4310
|
const sessionId = opts.resumeId || randomUUID();
|
|
4311
|
+
processes.claim(sessionId);
|
|
4475
4312
|
const diagnostics = new DiagnosticSink();
|
|
4476
4313
|
if (opts.hostPreset) {
|
|
4477
4314
|
await applyHostPreset({
|
|
@@ -4499,14 +4336,8 @@ async function assembleRuntime(opts, opened) {
|
|
|
4499
4336
|
...opts.interactive === void 0 ? {} : { interactive: opts.interactive },
|
|
4500
4337
|
...opts.logger ? { logger: opts.logger } : {},
|
|
4501
4338
|
...settingsPath ? { settingsPath } : {},
|
|
4502
|
-
// 宿主注入的技能目录(方案 44)。空数组也照传:`resolveSkillDirs` 对空数组
|
|
4503
|
-
// 和不给这个键的处理逐字相同,多一个分支只是多一处会走散的地方
|
|
4504
4339
|
hostSkillDirs: hostCaps.skillDirs,
|
|
4505
|
-
// 没有 `?? process.env.*` 的兜底,理由见 `BuildRuntimeOptions.managedSettingsPath`
|
|
4506
4340
|
...opts.managedSettingsPath ? { managedSettingsPath: opts.managedSettingsPath } : {},
|
|
4507
|
-
// **这一行必须紧跟着上面那次 `loadConfig()`**:`getConfigOrigins()` 报的是
|
|
4508
|
-
// 最近一次加载的账,中间隔一次别人的 `loadConfig()` 就串台了。递进去而不是
|
|
4509
|
-
// 让 `buildServices` 自己去问,判据写在 `ServiceOptions.configOrigins` 上
|
|
4510
4341
|
configOrigins
|
|
4511
4342
|
});
|
|
4512
4343
|
const config = services.config;
|
|
@@ -4545,21 +4376,25 @@ async function assembleRuntime(opts, opened) {
|
|
|
4545
4376
|
if (sandbox) {
|
|
4546
4377
|
cleanups.push(() => sandbox.destroy());
|
|
4547
4378
|
isolation = sandbox.getIsolation();
|
|
4548
|
-
diagnostics.ok("
|
|
4379
|
+
diagnostics.ok("SandboxIsolation", isolation.detail);
|
|
4549
4380
|
services.permission?.setSandboxIsolation(isolation.level);
|
|
4550
4381
|
}
|
|
4551
4382
|
if (services.permission) {
|
|
4552
4383
|
applyHeadless(services.permission, config, opts.headless, diagnostics, opts.interactive);
|
|
4553
4384
|
}
|
|
4554
4385
|
const budgetWiring = wireBudget(config, services.modelPricing, diagnostics);
|
|
4386
|
+
const bootstrapReveal = createToolRevealState();
|
|
4555
4387
|
const registry = registerLocalTools({
|
|
4556
4388
|
homeDir: config.homeDir,
|
|
4389
|
+
deferMcp: config.tools?.deferMcp === true,
|
|
4390
|
+
toolMode: foldToolMode(config.tools?.mode),
|
|
4391
|
+
bootstrapReveal,
|
|
4557
4392
|
trackerDb: services.trackerDb,
|
|
4558
4393
|
goals: services.goals,
|
|
4559
4394
|
memory: services.memory,
|
|
4560
4395
|
sandbox,
|
|
4396
|
+
skills: services.skill,
|
|
4561
4397
|
diags: diagnostics,
|
|
4562
|
-
// 合并后那份 config —— 项目级设置将来若放开 search 段,这里天然跟着走
|
|
4563
4398
|
...config.search ? { search: config.search } : {}
|
|
4564
4399
|
});
|
|
4565
4400
|
registerSessionSearch(registry, {
|
|
@@ -4568,10 +4403,16 @@ async function assembleRuntime(opts, opened) {
|
|
|
4568
4403
|
...config.sessionSearch ? { sessionSearch: config.sessionSearch } : {},
|
|
4569
4404
|
diags: diagnostics
|
|
4570
4405
|
});
|
|
4571
|
-
const mcpWiring = await registerMcpTools(
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4406
|
+
const mcpWiring = await registerMcpTools(
|
|
4407
|
+
registry,
|
|
4408
|
+
config.homeDir,
|
|
4409
|
+
diagnostics,
|
|
4410
|
+
[
|
|
4411
|
+
{ source: "host", servers: hostCaps.mcpServers },
|
|
4412
|
+
{ source: "plugin", servers: wirePluginMcpServers(services.plugins.mcpPaths, diagnostics) }
|
|
4413
|
+
],
|
|
4414
|
+
config.tools?.deferMcp === true
|
|
4415
|
+
);
|
|
4575
4416
|
cleanups.push(mcpWiring.dispose);
|
|
4576
4417
|
const bridges = new SessionBridges(opts.onApprovalRequest);
|
|
4577
4418
|
const interactive = !services.permission?.isNonInteractive();
|
|
@@ -4586,35 +4427,36 @@ async function assembleRuntime(opts, opened) {
|
|
|
4586
4427
|
permission: services.permission,
|
|
4587
4428
|
planState,
|
|
4588
4429
|
managed: services.managedPolicy,
|
|
4589
|
-
|
|
4430
|
+
deferredTools: () => registry.byExposure(ToolExposure.Deferred),
|
|
4431
|
+
bootstrapReveal,
|
|
4590
4432
|
startLevel: services.permissionLevel,
|
|
4591
|
-
// **和上面那两个进程级工具收的是同一个回调**:「弹给谁看」由
|
|
4592
|
-
// `ToolContext.sessionId` 决定,在这儿另写一个闭包等于把那条路由绕过去
|
|
4593
4433
|
requestApproval: requestPlanApproval,
|
|
4594
4434
|
interactive
|
|
4595
4435
|
});
|
|
4596
4436
|
const bootstrapGuard = guards.bootstrap();
|
|
4597
4437
|
registerAskQuestion(registry, (req, sessionId2) => bridges.ask(sessionId2, req), interactive);
|
|
4438
|
+
registry.registerTools(
|
|
4439
|
+
dropOverlongToolNames(
|
|
4440
|
+
hostCaps.tools,
|
|
4441
|
+
HOST_CAPABILITIES_MODULE,
|
|
4442
|
+
"host-tool-name-too-long",
|
|
4443
|
+
diagnostics
|
|
4444
|
+
),
|
|
4445
|
+
"host"
|
|
4446
|
+
);
|
|
4598
4447
|
const commands = wireCommands({
|
|
4599
4448
|
homeDir: config.homeDir,
|
|
4600
4449
|
projectRoot: services.projectContext.rootDir,
|
|
4601
4450
|
trusted: services.trust.trusted,
|
|
4602
4451
|
pluginDirs: services.plugins.commandDirs,
|
|
4603
4452
|
toolProvider: registry.asProvider(),
|
|
4604
|
-
// 传的是**函数**:命令展开发生在用户敲下那一刻,而这之前他可能已经
|
|
4605
|
-
// `/model` 换过了。取值钉住就等于永远解析成启动时那个模型
|
|
4606
|
-
//
|
|
4607
|
-
// 只递 provider + model 两段:`origin` 讲的是**当前选择**怎么来的,
|
|
4608
|
-
// 而这里回答的是「工具模型是谁」。原样带过去,命令会展开出一个
|
|
4609
|
-
// `origin: 'config'` 的 ref,随后 `enterTurn` 再按 `command-frontmatter`
|
|
4610
|
-
// 盖一次 —— 同一个字段有两个说法,出事时看哪个都不对
|
|
4611
4453
|
utilityModel: () => {
|
|
4612
4454
|
const s = services.provider?.getUtilityInfo().selection;
|
|
4613
4455
|
return s ? { provider: s.provider, model: s.model } : void 0;
|
|
4614
4456
|
},
|
|
4615
4457
|
diags: diagnostics
|
|
4616
4458
|
});
|
|
4617
|
-
const checkpoints = new CheckpointManager({ workDir, homeDir: config.homeDir });
|
|
4459
|
+
const checkpoints = new CheckpointManager({ workDir: () => workDir, homeDir: config.homeDir });
|
|
4618
4460
|
const { providerBridge } = services;
|
|
4619
4461
|
const agentRoles = [];
|
|
4620
4462
|
const agentRoleNotices = [];
|
|
@@ -4633,8 +4475,11 @@ async function assembleRuntime(opts, opened) {
|
|
|
4633
4475
|
mainRole = pickMainRole(opts.agent ?? process.env.EPOCH_AGENT, roles);
|
|
4634
4476
|
if (mainRole) {
|
|
4635
4477
|
diagnostics.ok(
|
|
4636
|
-
"
|
|
4637
|
-
|
|
4478
|
+
"MainAgentRole",
|
|
4479
|
+
t("boot.main_role", {
|
|
4480
|
+
name: mainRole.name,
|
|
4481
|
+
tools: mainRole.tools ? t("boot.main_role_tools", { count: mainRole.tools.length }) : t("boot.main_role_no_narrow")
|
|
4482
|
+
})
|
|
4638
4483
|
);
|
|
4639
4484
|
}
|
|
4640
4485
|
}
|
|
@@ -4652,34 +4497,24 @@ async function assembleRuntime(opts, opened) {
|
|
|
4652
4497
|
provider: providerBridge,
|
|
4653
4498
|
models,
|
|
4654
4499
|
guards,
|
|
4655
|
-
// 引导会话的工作目录。**不能拿 `projectContext.rootDir` 顶替** ——
|
|
4656
|
-
// 在仓库子目录里启动是常态,顶替等于悄悄换了相对路径的解析基准
|
|
4657
4500
|
defaultWorkDir: workDir,
|
|
4658
|
-
// 收窄过的那一层(方案 23);未收窄那份给子 agent,判据见工厂的选项注释
|
|
4659
4501
|
toolProvider: commands.toolProvider,
|
|
4660
4502
|
rawToolProvider: registry.asProvider(),
|
|
4503
|
+
deferredTools: () => registry.byExposure(ToolExposure.Deferred),
|
|
4504
|
+
codeModeTools: () => registry.codeModeDeclared(),
|
|
4661
4505
|
telemetry,
|
|
4662
4506
|
budget: budgetWiring,
|
|
4663
4507
|
...opts.onApprovalRequest ? { hostApproval: opts.onApprovalRequest } : {},
|
|
4664
4508
|
workspaces,
|
|
4665
4509
|
bridges,
|
|
4666
4510
|
sessions: services.session,
|
|
4667
|
-
// 目标(方案 52)。**进程一份**,收窄成「这一段会话」发生在工厂里
|
|
4668
|
-
// (`buildGoalControl(…, () => session.sessionId)`)
|
|
4669
4511
|
goals: services.goals,
|
|
4670
4512
|
...opts.sessionSource ? { sessionSource: opts.sessionSource } : {},
|
|
4671
4513
|
mainRole: mainRole ?? null,
|
|
4672
4514
|
roles: () => agentRoles,
|
|
4673
|
-
// 后台任务的状态变化(方案 36 PR-2)。给函数不给数组 —— 任务状态每时每刻
|
|
4674
|
-
// 在变,快照拿到的是装配那一刻的。
|
|
4675
|
-
//
|
|
4676
|
-
// ⚠️ 2026-08-15 起它**按会话取**:工厂给每个会话拿它自己那一批(判据在
|
|
4677
|
-
// plugin-terminal 的 `background.ts` 文件头)。收窄之前每个会话每轮都被告知
|
|
4678
|
-
// 一批**它没起过**的命令在跑,而那条通知是直接进模型上下文的
|
|
4679
4515
|
backgroundTasks: (id) => listTasks(id),
|
|
4680
|
-
// 子任务跑完之后把它起的后台任务归还给派活的会话(判据在 `subRunner` 末尾)。
|
|
4681
|
-
// 这里是全仓唯一一处知道「任务表由 plugin-terminal 提供」的地方
|
|
4682
4516
|
rekeyBackgroundTasks: rekeyTasks,
|
|
4517
|
+
processes,
|
|
4683
4518
|
diags: diagnostics
|
|
4684
4519
|
});
|
|
4685
4520
|
if (providerBridge) {
|
|
@@ -4689,19 +4524,22 @@ async function assembleRuntime(opts, opened) {
|
|
|
4689
4524
|
for (const c of registry.getConflicts()) {
|
|
4690
4525
|
diagnostics.warn(
|
|
4691
4526
|
`Tool ${c.name}`,
|
|
4692
|
-
|
|
4527
|
+
t("boot.tool_conflict", { dropped: c.droppedFrom, kept: c.keptFrom }),
|
|
4693
4528
|
"tool-name-conflict"
|
|
4694
4529
|
);
|
|
4695
4530
|
}
|
|
4696
4531
|
}
|
|
4532
|
+
reportToolCost(registry, diagnostics);
|
|
4697
4533
|
const store = services.session ? new SessionStore(
|
|
4698
4534
|
services.session,
|
|
4699
4535
|
config.homeDir,
|
|
4700
|
-
services.projectContext.rootDir,
|
|
4536
|
+
() => services.projectContext.rootDir,
|
|
4701
4537
|
opts.sessionSource
|
|
4702
4538
|
) : void 0;
|
|
4703
4539
|
const history = opts.resumeId && store ? store.loadHistory(opts.resumeId) : [];
|
|
4704
|
-
if (history.length > 0)
|
|
4540
|
+
if (history.length > 0) {
|
|
4541
|
+
diagnostics.ok("History", t("boot.history_loaded", { count: history.length }));
|
|
4542
|
+
}
|
|
4705
4543
|
const bootstrap = factory.adopt(sessionId, history);
|
|
4706
4544
|
const session = bootstrap?.session ?? null;
|
|
4707
4545
|
const agent = bootstrap?.agent ?? null;
|
|
@@ -4728,7 +4566,7 @@ async function assembleRuntime(opts, opened) {
|
|
|
4728
4566
|
}
|
|
4729
4567
|
}
|
|
4730
4568
|
try {
|
|
4731
|
-
await
|
|
4569
|
+
await processes.release();
|
|
4732
4570
|
} catch {
|
|
4733
4571
|
}
|
|
4734
4572
|
};
|
|
@@ -4737,10 +4575,6 @@ async function assembleRuntime(opts, opened) {
|
|
|
4737
4575
|
const provider = services.provider;
|
|
4738
4576
|
const listTools = () => commands.toolProvider.list().map((tool) => ({
|
|
4739
4577
|
tool,
|
|
4740
|
-
// 注册表一直记着「谁注册的这个工具」,只是以前没人往外传。能力页的
|
|
4741
|
-
// 「内置连接器」那一组要按它分(方案 42 PR-1)—— 按工具名前缀猜
|
|
4742
|
-
// (`file_*` 归 plugin-file)会在第一个不按前缀命名的工具上错。
|
|
4743
|
-
// 取不到只可能是收窄层放进来一个 registry 没见过的名字,那时说 `builtin`
|
|
4744
4578
|
source: registry.sourceOf(tool.name) ?? "builtin"
|
|
4745
4579
|
}));
|
|
4746
4580
|
const schedules = createScheduleControl({
|
|
@@ -4748,30 +4582,35 @@ async function assembleRuntime(opts, opened) {
|
|
|
4748
4582
|
...opts.scheduleRunner ? { runner: opts.scheduleRunner } : {}
|
|
4749
4583
|
});
|
|
4750
4584
|
cleanups.push(schedules.dispose);
|
|
4585
|
+
if (schedules.openError) {
|
|
4586
|
+
diagnostics.failed("Schedules", `FAILED (${schedules.openError.message})`, "init-failed");
|
|
4587
|
+
} else {
|
|
4588
|
+
diagnostics.ok("Schedules");
|
|
4589
|
+
}
|
|
4590
|
+
const hostMarketplaces = opts.hostMarketplaces;
|
|
4591
|
+
if (hostMarketplaces) {
|
|
4592
|
+
await wireHostMarketplaces({
|
|
4593
|
+
hostMarketplaces,
|
|
4594
|
+
statePath: marketplacesPath(config.homeDir),
|
|
4595
|
+
diagnostics
|
|
4596
|
+
});
|
|
4597
|
+
}
|
|
4598
|
+
reportI18nHealth(diagnostics);
|
|
4751
4599
|
return {
|
|
4752
4600
|
agent,
|
|
4753
4601
|
session,
|
|
4754
|
-
// 多会话工厂(方案 30 §6.3)。`SessionFactory` 是收窄过的那一面 ——
|
|
4755
|
-
// `adopt` / `subRunner` 是装配层的特权,宿主拿到它就等于能绕过 `bind()`
|
|
4756
|
-
// 那两道门(判据同 `WorkspaceControl` 上刻意不露 `seed` 的那一条)
|
|
4757
4602
|
sessionFactory: factory,
|
|
4603
|
+
events: processEventBus,
|
|
4758
4604
|
sessionStore: store ?? null,
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
// `??` 那一支只在 provider 没起来时走得到(那时一个会话都装配不出来):
|
|
4765
|
-
// 这个字段不可为 null,而一个读得出空列表的控制面是那时唯一诚实的答案
|
|
4605
|
+
sessionReferences: services.session?.references ?? null,
|
|
4606
|
+
workspaceFiles: buildWorkspaceFilesControl(
|
|
4607
|
+
{ listFiles: listWorkspaceFiles, resolveInWorkspace },
|
|
4608
|
+
services.permission
|
|
4609
|
+
),
|
|
4766
4610
|
checkpoints: bootstrap?.checkpoints ?? buildCheckpointControl(checkpoints, null, () => sessionId),
|
|
4767
|
-
// **引导会话自己那一份**,判据逐字同上面的 `checkpoints`:从工厂手里拿,
|
|
4768
|
-
// 不在这儿再包一个。`??` 那一支同理只在 provider 没起来时走得到 ——
|
|
4769
|
-
// 那时候没有会话,而目标是会话的状态,`null` 是那一刻唯一诚实的答案
|
|
4770
4611
|
goals: bootstrap?.goals ?? (services.goals ? buildGoalControl(services.goals, () => sessionId) : null),
|
|
4612
|
+
goalCatalog: services.goals ? buildGoalCatalog(services.goals) : null,
|
|
4771
4613
|
config,
|
|
4772
|
-
// 这次实际生效的数据目录,以及从它派生的 artifact 根目录(方案 54 §二)。
|
|
4773
|
-
// **调 `paths.ts` 而不是 `join(homeDir, 'artifacts')`**:路径真源只有一处,
|
|
4774
|
-
// 在这里拼字符串就是给同一个目录第二个说法 —— 而那正是这一节要修的病
|
|
4775
4614
|
homeDir: config.homeDir,
|
|
4776
4615
|
artifactsRoot: artifactsDir(void 0, config.homeDir),
|
|
4777
4616
|
sessionId,
|
|
@@ -4779,22 +4618,11 @@ async function assembleRuntime(opts, opened) {
|
|
|
4779
4618
|
schedules: schedules.control,
|
|
4780
4619
|
workspace,
|
|
4781
4620
|
workspaces,
|
|
4782
|
-
// 装配过程里到处在用的那个布尔(指令文件、角色文件、自定义命令都问它),
|
|
4783
|
-
// 原样露出来给 doctor —— 别在宿主侧重新判一遍,见字段的 JSDoc
|
|
4784
4621
|
trusted: services.trust.trusted,
|
|
4785
|
-
// 信任记录的读写口(方案 54 §五)。**零新逻辑** —— 转的就是装配时那个
|
|
4786
|
-
// manager 实例本身,宿主 `record()` 之后本进程新绑的工作区立刻按新判定走
|
|
4787
|
-
// (闸门每次现问 `check()`,中间没有缓存)。
|
|
4788
|
-
//
|
|
4789
|
-
// 转实例而不是包一层收窄:四个方法就是 protocol 的 `TrustManager` 契约,
|
|
4790
|
-
// 包一层只会多出一处「哪天签名变了这里忘了跟」。
|
|
4791
4622
|
trust: services.trustManager,
|
|
4792
4623
|
isolation,
|
|
4793
4624
|
policy: services.policy,
|
|
4794
|
-
|
|
4795
|
-
// 「模型现在看得见什么」,而一条写了 `allowed-tools` 的自定义命令正在跑的时候,
|
|
4796
|
-
// 模型看得见的就是白名单里那几个。读原始 registry 的话 `/tools` 会在收窄期间说假话。
|
|
4797
|
-
// getter 而不是快照:MCP 刷新(方案 15)和收窄进出都要能立刻反映出来
|
|
4625
|
+
hook: services.hook,
|
|
4798
4626
|
get tools() {
|
|
4799
4627
|
return listTools().map(({ tool, source }) => ({
|
|
4800
4628
|
name: tool.name,
|
|
@@ -4804,19 +4632,10 @@ async function assembleRuntime(opts, opened) {
|
|
|
4804
4632
|
},
|
|
4805
4633
|
usageScope: budgetWiring.budgetStore ? "session" : "run",
|
|
4806
4634
|
compression: compressionLine(config, services),
|
|
4807
|
-
// **getter 而不是快照**(和上面的 `tools` 同一个理由):方案 26 之后
|
|
4808
|
-
// `/model` 能在运行期换 provider,拍一张快照的话 `/model` 会一直显示
|
|
4809
|
-
// 启动时那家和那把 key —— 一个专门用来"确认我现在用的是什么"的面板在说假话
|
|
4810
4635
|
get providerInfo() {
|
|
4811
4636
|
return provider ? { provider: provider.getProviderType(), apiKeyHint: provider.getApiKeyHint() } : null;
|
|
4812
4637
|
},
|
|
4813
|
-
// **引导会话那一份**(2026-08-16)——它就是这个进程那个 router 的出口本体,
|
|
4814
|
-
// 所以 TUI 的 `/model`、CLI 的 `--model` 一个字节都没变。别的会话走
|
|
4815
|
-
// `sessionFactory.get(id).model`,判据见 [session-model.ts](./session-model.ts)
|
|
4816
4638
|
model: bootstrapModel?.control ?? null,
|
|
4817
|
-
// **不吃 `provider`**,所以 provider 层起不来时它照样在(判据在字段上)。
|
|
4818
|
-
// `configured` 写成 getter 而不是拍下来的两个值:base URL 只对配置里那一家
|
|
4819
|
-
// 有效,而那条判断住在 `model-catalog.ts` 里,不在这儿
|
|
4820
4639
|
modelCatalog: buildModelCatalogControl({
|
|
4821
4640
|
homeDir: config.homeDir,
|
|
4822
4641
|
configured: () => ({
|
|
@@ -4824,51 +4643,46 @@ async function assembleRuntime(opts, opened) {
|
|
|
4824
4643
|
...config.provider.baseUrl ? { baseUrl: config.provider.baseUrl } : {}
|
|
4825
4644
|
})
|
|
4826
4645
|
}),
|
|
4646
|
+
utility: provider ? buildUtilityControl({
|
|
4647
|
+
provider,
|
|
4648
|
+
...budgetWiring.budgetStore ? { store: budgetWiring.budgetStore } : {},
|
|
4649
|
+
...budgetWiring.pricingOf ? { pricingOf: budgetWiring.pricingOf } : {}
|
|
4650
|
+
}) : null,
|
|
4827
4651
|
agentRoles,
|
|
4828
4652
|
agentRoleNotices,
|
|
4829
4653
|
get skills() {
|
|
4830
4654
|
return services.skill?.list() ?? [];
|
|
4831
4655
|
},
|
|
4832
|
-
// 不计数的那条读(方案 56 §1.2)。`?? null` 而不是 `?? ''`:技能系统没起来
|
|
4833
|
-
// 和「这个技能的正文是空的」是两件事,合成同一个值之后界面画不出前者
|
|
4834
4656
|
skillBody: (name) => services.skill?.body(name) ?? null,
|
|
4835
|
-
|
|
4836
|
-
|
|
4657
|
+
skillIndexResidency: (role) => services.skill?.indexResidency({
|
|
4658
|
+
only: role === null ? void 0 : findRole(agentRoles, role)?.skills
|
|
4659
|
+
}) ?? /* @__PURE__ */ new Map(),
|
|
4837
4660
|
skillImport: buildSkillControl(services),
|
|
4838
|
-
// 身份那一栏的写口(2026-08-18)。**只露一个 `add`**,同上面那两条。
|
|
4839
|
-
// ⚠️ `reload` 递的是上面那个 `reloadRoles` —— 建完当场重载,判据在
|
|
4840
|
-
// `role-write.ts` 文件头第二节(不重载的话这一下只做了一半)
|
|
4841
4661
|
roleWrite: createRoleWriteControl({
|
|
4842
4662
|
userDir: agentsDir(config.homeDir),
|
|
4843
4663
|
roles: () => agentRoles,
|
|
4844
|
-
// 给函数不给快照:MCP 连上 / 掉线都会让工具表变,而工具名要按**用户按下
|
|
4845
|
-
// 那一刻**的那份判(同 `mcpServers` 是个 getter 的判据)
|
|
4846
4664
|
tools: () => registry.asProvider(),
|
|
4847
4665
|
reload: reloadRoles
|
|
4848
4666
|
}),
|
|
4849
|
-
|
|
4667
|
+
plugins: hostMarketplaces ? createPluginControl({
|
|
4668
|
+
pluginsDir: pluginsDir(config.homeDir),
|
|
4669
|
+
statePath: pluginsStatePath(config.homeDir),
|
|
4670
|
+
marketplacesPath: marketplacesPath(config.homeDir),
|
|
4671
|
+
loaded: services.plugins.loaded,
|
|
4672
|
+
allowRemote: hostMarketplaces.allowRemote === true
|
|
4673
|
+
}) : null,
|
|
4850
4674
|
get mcpServers() {
|
|
4851
4675
|
return mcpWiring.status();
|
|
4852
4676
|
},
|
|
4853
|
-
// 收窄过的那一面(方案 56 §1.1)。**转的是 `McpWiring.control` 而不是
|
|
4854
|
-
// registry 本身** —— 判据在 `McpControl` 的文档上,同 `WorkspaceControl`
|
|
4855
|
-
// 刻意不露 `seed()` 那一条
|
|
4856
4677
|
mcp: mcpWiring.control,
|
|
4857
4678
|
memory: services.memory ? buildMemoryControl(services.memory) : null,
|
|
4858
|
-
// 引导会话那一份档位(`level` / `setLevel`)+ 三样进程级的读
|
|
4859
|
-
// (规则表 / 遮蔽 / 审计流水)。⚠️ 换档那三步(托管挡 bypass → `setLevel` →
|
|
4860
|
-
// `plan.forget()`)**不在这个函数里**:它们住在 `session-guard.ts`,
|
|
4861
|
-
// 因为它们现在要按会话跑 N 遍,而那条判定只能有一个实现
|
|
4862
4679
|
permissions: buildPermissionsControl(services, bootstrapGuard.permissions, listTools),
|
|
4863
|
-
// 设置页那一片。**收 `workspaces`** 是这一轮补的那笔账:③④ 跟着工作区走,
|
|
4864
|
-
// 而工作区一个会话绑一个(决定 18)—— 判据在 `SettingsControl` 的文档上
|
|
4865
4680
|
settings: buildSettingsControl(services, workspaces, {
|
|
4866
4681
|
...settingsPath ? { settingsPath } : {},
|
|
4867
4682
|
...opts.managedSettingsPath ? { managedSettingsPath: opts.managedSettingsPath } : {}
|
|
4868
4683
|
}),
|
|
4869
|
-
// 同 `model`:**引导会话那一份**,而它就是进程那个 `PlanModeState` 的出口本体
|
|
4870
4684
|
plan: bootstrapGuard.plan,
|
|
4871
|
-
sessions: services.session ? buildSessionControl(services.session, session, checkpoints, (from, id) => {
|
|
4685
|
+
sessions: services.session ? buildSessionControl(services.session, session, checkpoints, config.homeDir, (from, id) => {
|
|
4872
4686
|
planState?.restore(store?.loadPlan(id) ?? null);
|
|
4873
4687
|
workspaces.seed(id, {
|
|
4874
4688
|
workspace,
|
|
@@ -4879,7 +4693,6 @@ async function assembleRuntime(opts, opened) {
|
|
|
4879
4693
|
factory.rekey(source, id);
|
|
4880
4694
|
rekeyTasks(source, id);
|
|
4881
4695
|
}) : null,
|
|
4882
|
-
// getter 而不是快照,理由同 `tools` / `providerInfo`
|
|
4883
4696
|
get contextBreakdown() {
|
|
4884
4697
|
if (!agent) return null;
|
|
4885
4698
|
const historyText = (session?.getHistory() ?? []).map((m) => m.content).join("\n");
|
|
@@ -4889,18 +4702,12 @@ async function assembleRuntime(opts, opened) {
|
|
|
4889
4702
|
});
|
|
4890
4703
|
},
|
|
4891
4704
|
commands: commands.control,
|
|
4892
|
-
// 用 `registry` 而不是 `commands.toolProvider`:`!` 是**用户**敲的,
|
|
4893
|
-
// 不该被一条自定义命令的 `allowed-tools` 收窄掉(那条收窄的语义是
|
|
4894
|
-
// 「这一轮**模型**能用什么」)
|
|
4895
4705
|
userActions: createUserActions({
|
|
4896
4706
|
registry,
|
|
4897
4707
|
permission: services.permission,
|
|
4898
4708
|
workDir,
|
|
4899
4709
|
sessionId
|
|
4900
4710
|
}),
|
|
4901
|
-
// 三个通道,**一个真源**(那个 sink):字符串那份和「换个语言说一遍」那份
|
|
4902
|
-
// 都是投影,不可能漂移。前两个是快照(装配到这一步就定了),
|
|
4903
|
-
// 第三个是函数 —— 它要等请求到了才知道用哪个语言
|
|
4904
4711
|
diagnostics: diagnostics.toStrings(),
|
|
4905
4712
|
diagnosticList: diagnostics.toList(),
|
|
4906
4713
|
diagnosticsIn: (lang) => diagnostics.toList(lang),
|
|
@@ -4933,9 +4740,9 @@ function wireAgentRoles(config, services, registry, diags, hostRoles = []) {
|
|
|
4933
4740
|
});
|
|
4934
4741
|
const external = [...services.plugins.roles, ...hostRoles, ...load.overrides];
|
|
4935
4742
|
const { roles, notices } = mergeRoles(external);
|
|
4936
|
-
for (const n of notices) diags.warn(
|
|
4743
|
+
for (const n of notices) diags.warn(AGENT_ROLE_DIAG_MODULE, n.detail, n.code);
|
|
4937
4744
|
if (!load.projectSkipped) {
|
|
4938
|
-
diags.ok(
|
|
4745
|
+
diags.ok(AGENT_ROLE_DIAG_MODULE, (lang) => {
|
|
4939
4746
|
const parts = [
|
|
4940
4747
|
t("agent_roles.tally_builtin", { count: builtinRoles().length }, lang),
|
|
4941
4748
|
t("agent_roles.tally_user", { count: load.userCount }, lang),
|
|
@@ -4955,19 +4762,27 @@ function wireAgentRoles(config, services, registry, diags, hostRoles = []) {
|
|
|
4955
4762
|
const unknown = unknownRoleTools(provider, role, BLOCKED_TOOLS);
|
|
4956
4763
|
if (unknown.length > 0) {
|
|
4957
4764
|
diags.warn(
|
|
4958
|
-
|
|
4959
|
-
|
|
4765
|
+
AGENT_ROLE_DIAG_MODULE,
|
|
4766
|
+
t("boot.role_unknown_tools", { role: role.name, tools: unknown.join(", ") }),
|
|
4960
4767
|
"unknown-tool"
|
|
4961
4768
|
);
|
|
4962
4769
|
}
|
|
4963
4770
|
const hitBlocked = blockedRoleTools(role, BLOCKED_TOOLS);
|
|
4964
4771
|
if (hitBlocked.length > 0) {
|
|
4965
4772
|
diags.warn(
|
|
4966
|
-
|
|
4967
|
-
|
|
4773
|
+
AGENT_ROLE_DIAG_MODULE,
|
|
4774
|
+
t("boot.role_blocked_tools", { role: role.name, tools: hitBlocked.join(", ") }),
|
|
4968
4775
|
"role-blocked-tool"
|
|
4969
4776
|
);
|
|
4970
4777
|
}
|
|
4778
|
+
const unknownSkills = unknownRoleSkills(services.skill?.getNames() ?? [], role);
|
|
4779
|
+
if (unknownSkills.length > 0) {
|
|
4780
|
+
diags.warn(
|
|
4781
|
+
AGENT_ROLE_DIAG_MODULE,
|
|
4782
|
+
t("boot.role_unknown_skills", { role: role.name, skills: unknownSkills.join(", ") }),
|
|
4783
|
+
"unknown-skill"
|
|
4784
|
+
);
|
|
4785
|
+
}
|
|
4971
4786
|
}
|
|
4972
4787
|
return { roles, notices };
|
|
4973
4788
|
}
|
|
@@ -4977,7 +4792,9 @@ function pickMainRole(wanted, roles) {
|
|
|
4977
4792
|
const role = findRole(roles, name);
|
|
4978
4793
|
if (role) return role;
|
|
4979
4794
|
const available = roles.map((r) => r.name).join(", ");
|
|
4980
|
-
throw new AgentRoleError(
|
|
4795
|
+
throw new AgentRoleError(
|
|
4796
|
+
t("boot.role_not_found", { name, available: available || t("boot.role_none") })
|
|
4797
|
+
);
|
|
4981
4798
|
}
|
|
4982
4799
|
function makePricingLookup(homeDir) {
|
|
4983
4800
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -5005,18 +4822,18 @@ function wireBudget(config, modelPricing, diagnostics) {
|
|
|
5005
4822
|
);
|
|
5006
4823
|
const { maxCostUsd, maxTokens } = config.budget ?? {};
|
|
5007
4824
|
if (maxCostUsd === void 0 && maxTokens === void 0) {
|
|
5008
|
-
diagnostics.skipped("Budget", "
|
|
4825
|
+
diagnostics.skipped("Budget", t("boot.budget_none"));
|
|
5009
4826
|
} else {
|
|
5010
4827
|
const limits = [
|
|
5011
|
-
maxCostUsd !== void 0 ?
|
|
4828
|
+
maxCostUsd !== void 0 ? t("boot.budget_cost", { amount: maxCostUsd }) : "",
|
|
5012
4829
|
maxTokens !== void 0 ? `token ${maxTokens}` : ""
|
|
5013
4830
|
].filter(Boolean).join(" / ");
|
|
5014
|
-
diagnostics.ok("Budget",
|
|
4831
|
+
diagnostics.ok("Budget", t("boot.budget_limits", { limits }));
|
|
5015
4832
|
}
|
|
5016
4833
|
if (maxCostUsd !== void 0 && !modelPricing) {
|
|
5017
4834
|
diagnostics.warn(
|
|
5018
4835
|
"Budget",
|
|
5019
|
-
|
|
4836
|
+
t("boot.budget_no_pricing", { model: config.model }),
|
|
5020
4837
|
"no-pricing-data"
|
|
5021
4838
|
);
|
|
5022
4839
|
}
|
|
@@ -5024,8 +4841,6 @@ function wireBudget(config, modelPricing, diagnostics) {
|
|
|
5024
4841
|
...config.budget ? { budget: config.budget } : {},
|
|
5025
4842
|
...modelPricing ? { modelPricing } : {},
|
|
5026
4843
|
...budgetStore ? { budgetStore } : {},
|
|
5027
|
-
// **无条件给**,不看有没有配上限:闸门就算不拦截也要算钱(usage 事件里的
|
|
5028
|
-
// costUsd 有消费方),算错了一样是错的
|
|
5029
4844
|
pricingOf: makePricingLookup(config.homeDir)
|
|
5030
4845
|
};
|
|
5031
4846
|
}
|
|
@@ -5037,7 +4852,7 @@ function applyHeadless(permission, config, override, diagnostics, interactive) {
|
|
|
5037
4852
|
if (nonInteractive) {
|
|
5038
4853
|
diagnostics.warn(
|
|
5039
4854
|
"Headless",
|
|
5040
|
-
|
|
4855
|
+
t("boot.headless_preauth", { policy: describeHeadlessPolicy(merged) }),
|
|
5041
4856
|
"headless-preauthorized"
|
|
5042
4857
|
);
|
|
5043
4858
|
}
|
|
@@ -5062,16 +4877,17 @@ async function migrateOnce(store, diagnostics, override) {
|
|
|
5062
4877
|
envFilePath: envPath(homeDir),
|
|
5063
4878
|
store,
|
|
5064
4879
|
names: API_KEY_ENV_VARS,
|
|
5065
|
-
// 只到天:同一天多跑几次不该产生一堆备份文件,真重名时由 migrate.ts 自己让路
|
|
5066
4880
|
stamp: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")
|
|
5067
4881
|
});
|
|
5068
4882
|
if (result.warning) diagnostics.warn("Secret", result.warning, "secret-migrate");
|
|
5069
4883
|
if (result.migrated.length > 0) {
|
|
5070
4884
|
diagnostics.ok(
|
|
5071
4885
|
"Secret",
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
4886
|
+
t("boot.secret_migrated", {
|
|
4887
|
+
count: result.migrated.length,
|
|
4888
|
+
backend: store.detail,
|
|
4889
|
+
backup: result.backupPath ? t("boot.secret_migrated_backup", { path: result.backupPath }) : ""
|
|
4890
|
+
})
|
|
5075
4891
|
);
|
|
5076
4892
|
}
|
|
5077
4893
|
}
|
|
@@ -5093,7 +4909,7 @@ async function setupMcpDataKey(store, diagnostics) {
|
|
|
5093
4909
|
const why = err instanceof Error ? err.message : String(err);
|
|
5094
4910
|
diagnostics.warn(
|
|
5095
4911
|
"Secret",
|
|
5096
|
-
|
|
4912
|
+
t("boot.mcp_key_unavailable", { reason: why }),
|
|
5097
4913
|
"mcp-key-unavailable"
|
|
5098
4914
|
);
|
|
5099
4915
|
}
|
|
@@ -5136,19 +4952,25 @@ function buildMemoryControl(memory) {
|
|
|
5136
4952
|
return {
|
|
5137
4953
|
list: () => memory.getAllEntries().map((e) => ({
|
|
5138
4954
|
content: e.content,
|
|
5139
|
-
// `getAllEntries` 不带 target,而这一层要显示「记在哪一级」。
|
|
5140
|
-
// 拿不到就说 unknown,不猜 —— 猜错会让用户去错的文件里找
|
|
5141
4955
|
target: e.target ?? "unknown"
|
|
5142
4956
|
})),
|
|
5143
4957
|
remove: async (content) => {
|
|
5144
4958
|
const trimmed = content.trim();
|
|
5145
4959
|
const hit = memory.getAllEntries().find((e) => e.content.includes(trimmed));
|
|
5146
|
-
if (!hit) return { ok: false, message:
|
|
4960
|
+
if (!hit) return { ok: false, message: t("boot.memory_not_found", { query: trimmed }) };
|
|
5147
4961
|
try {
|
|
5148
4962
|
await memory.remove("memory", hit.content);
|
|
5149
|
-
return {
|
|
4963
|
+
return {
|
|
4964
|
+
ok: true,
|
|
4965
|
+
message: t("boot.memory_removed", { content: hit.content.slice(0, 60) })
|
|
4966
|
+
};
|
|
5150
4967
|
} catch (err) {
|
|
5151
|
-
return {
|
|
4968
|
+
return {
|
|
4969
|
+
ok: false,
|
|
4970
|
+
message: t("boot.memory_remove_failed", {
|
|
4971
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
4972
|
+
})
|
|
4973
|
+
};
|
|
5152
4974
|
}
|
|
5153
4975
|
}
|
|
5154
4976
|
};
|
|
@@ -5163,14 +4985,11 @@ function toSessionSummary(meta) {
|
|
|
5163
4985
|
messageCount: meta.messageCount,
|
|
5164
4986
|
model: meta.model,
|
|
5165
4987
|
costUsd: meta.estimatedCostUsd,
|
|
5166
|
-
// 两格原样转发,这一层**不判也不补**:库里是 NULL 就是 `undefined`
|
|
5167
|
-
// (`rowToMeta()` 在那两格上专门做了 `?? undefined` 的归一,判据写在那儿),
|
|
5168
|
-
// 而「没记过」和「明确不使用」在宿主那一侧是两个组
|
|
5169
4988
|
workspaceState: meta.workspaceState,
|
|
5170
4989
|
workspaceRoot: meta.workspaceRoot
|
|
5171
4990
|
};
|
|
5172
4991
|
}
|
|
5173
|
-
function buildSessionControl(sessions, session, checkpoints, onResumed) {
|
|
4992
|
+
function buildSessionControl(sessions, session, checkpoints, homeDir, onResumed) {
|
|
5174
4993
|
return {
|
|
5175
4994
|
resume: (sessionId) => {
|
|
5176
4995
|
const from = session?.sessionId ?? null;
|
|
@@ -5178,10 +4997,6 @@ function buildSessionControl(sessions, session, checkpoints, onResumed) {
|
|
|
5178
4997
|
onResumed?.(from, sessionId);
|
|
5179
4998
|
return restored;
|
|
5180
4999
|
},
|
|
5181
|
-
// ⚠️ `sortKey: 'updatedAt'` 这一行**没改,但含义变了**(会话库 v4):它以前
|
|
5182
|
-
// 在 `SessionDB.listSessions` 里静默退化成 `started_at`,现在真的按最后活动排。
|
|
5183
|
-
// 于是这个 `limit` 截出来的那批也变了 —— 一段两年前建、今天还在聊的会话
|
|
5184
|
-
// 从此进得了列表,而它以前必然被一批「新建但没说过话」的会话挤掉
|
|
5185
5000
|
list: (limit = 20) => sessions.list({ limit, sortKey: "updatedAt", direction: "desc" }).sessions.map(toSessionSummary),
|
|
5186
5001
|
get: (sessionId) => {
|
|
5187
5002
|
const found = sessions.get(sessionId);
|
|
@@ -5201,7 +5016,13 @@ function buildSessionControl(sessions, session, checkpoints, onResumed) {
|
|
|
5201
5016
|
} catch {
|
|
5202
5017
|
checkpointsRemoved = false;
|
|
5203
5018
|
}
|
|
5204
|
-
|
|
5019
|
+
let artifactsRemoved = true;
|
|
5020
|
+
try {
|
|
5021
|
+
await removeSessionArtifacts(sessionId, homeDir);
|
|
5022
|
+
} catch {
|
|
5023
|
+
artifactsRemoved = false;
|
|
5024
|
+
}
|
|
5025
|
+
return { deleted, checkpointsRemoved, artifactsRemoved };
|
|
5205
5026
|
},
|
|
5206
5027
|
search: (query, limit = 20) => sessions.search({ query, limit, scope: "all" }).results.flatMap(
|
|
5207
5028
|
(r) => r.matches.map((m) => ({
|
|
@@ -5238,15 +5059,17 @@ function buildPermissionsControl(services, bootstrap, tools) {
|
|
|
5238
5059
|
rules: effectiveRules,
|
|
5239
5060
|
shadows: () => checkShadowing(compileRuleLists(services.config.permissions)),
|
|
5240
5061
|
level: () => bootstrap?.level() ?? services.config.permission,
|
|
5241
|
-
// 配置里那一档 + 它赢在哪一层(方案 56 §1.3)。**和上面那一格是两个值** ——
|
|
5242
|
-
// 判据在接口上。读 `baseConfig` 是因为 `configOrigins` 记的就是它那一份
|
|
5243
|
-
// (而 `permission` 不在 `SettingsFileSchema` 白名单里,两份在这个键上恒等)
|
|
5244
5062
|
configured: () => ({
|
|
5245
5063
|
level: services.baseConfig.permission,
|
|
5246
5064
|
layer: originOf(services.configOrigins, "permission")
|
|
5247
5065
|
}),
|
|
5248
|
-
// 权限层起不来 = 什么都没判过,回空账而不是 null(见接口上那段注释)
|
|
5249
5066
|
audit: (scope) => services.permission?.getAudit(scope) ?? { entries: [], dropped: 0 },
|
|
5067
|
+
approvals: () => services.permission?.listApprovals() ?? [],
|
|
5068
|
+
revokeApproval: (id) => {
|
|
5069
|
+
const perm = services.permission;
|
|
5070
|
+
if (!perm) return { ok: false, reason: "no-permission-layer" };
|
|
5071
|
+
return perm.revokeApproval(id) ? { ok: true } : { ok: false, reason: "unknown-id" };
|
|
5072
|
+
},
|
|
5250
5073
|
managed: () => {
|
|
5251
5074
|
const p = services.managedPolicy;
|
|
5252
5075
|
return {
|
|
@@ -5257,9 +5080,6 @@ function buildPermissionsControl(services, bootstrap, tools) {
|
|
|
5257
5080
|
bypassDisabled: p.disableBypassPermissionsMode
|
|
5258
5081
|
};
|
|
5259
5082
|
},
|
|
5260
|
-
// 三步判定**整个转发**,一行都不在这里重写(判据见上面那个参数的注释)。
|
|
5261
|
-
// 权限层没起来时回 `{ok:true}` 而不是拒绝:那时 `level()` 读的是
|
|
5262
|
-
// `config.permission`,一个纯展示值,改它没有任何意义也没有任何危害
|
|
5263
5083
|
setLevel: (level) => bootstrap?.setLevel(level) ?? { ok: true },
|
|
5264
5084
|
gates: (level) => {
|
|
5265
5085
|
const rules = compileRuleLists(services.config.permissions);
|
|
@@ -5283,12 +5103,7 @@ function compressionLine(config, services) {
|
|
|
5283
5103
|
const resolved = compressionOptions(config);
|
|
5284
5104
|
return {
|
|
5285
5105
|
contextLength: services.contextLength,
|
|
5286
|
-
// `.parse({})` 而不是 `defaultConfig()`:后者的返回类型标注成 `EpochConfig`,
|
|
5287
|
-
// 而那上面 `compression` 是可选的(嵌入宿主手搓那一份走的就是这条),
|
|
5288
|
-
// 于是这个回落自己又需要一个回落。schema 的产出类型上它是必有的(`.prefault({})`)
|
|
5289
5106
|
threshold: resolved.threshold ?? EpochConfigSchema.parse({}).compression.threshold,
|
|
5290
|
-
// `!== false` 而不是裸真值:判据同 `compressionOptions()` 里那一行 ——
|
|
5291
|
-
// 只有显式的 false 才关得掉,而这一条要写在脸上
|
|
5292
5107
|
auto: services.contextEngine !== null && resolved.enabled !== false
|
|
5293
5108
|
};
|
|
5294
5109
|
}
|
|
@@ -5313,11 +5128,6 @@ function buildSettingsControl(services, workspaces, opts) {
|
|
|
5313
5128
|
return {
|
|
5314
5129
|
...chain,
|
|
5315
5130
|
overridable,
|
|
5316
|
-
// 「写进去会不会生效」整个判断在 core,这一层只递参数 —— 它必须和
|
|
5317
|
-
// `write()` 里那一次用同一个函数算,否则界面警告和落盘结论会分叉。
|
|
5318
|
-
//
|
|
5319
|
-
// ⚠️ `paths` 和 `trusted` **来自同一个 `scope`**,那是硬约束不是巧合:
|
|
5320
|
-
// 判据在 `SettingsControl` 的文档和 core 的 `config/write.ts` 第四节
|
|
5321
5131
|
writes: scope.paths === null ? [] : settingWriteTargets({
|
|
5322
5132
|
winner: chain.layer,
|
|
5323
5133
|
overridable,
|
|
@@ -5409,13 +5219,25 @@ function isSettingValue(value) {
|
|
|
5409
5219
|
function maybeValue(key, value) {
|
|
5410
5220
|
return value === void 0 ? {} : { [key]: value };
|
|
5411
5221
|
}
|
|
5222
|
+
function reportI18nHealth(diagnostics) {
|
|
5223
|
+
void t("i18n.locales_missing");
|
|
5224
|
+
const issues = i18nDiagnostics();
|
|
5225
|
+
if (issues.length === 0) {
|
|
5226
|
+
diagnostics.ok("I18n", "OK");
|
|
5227
|
+
return;
|
|
5228
|
+
}
|
|
5229
|
+
for (const issue of issues) diagnostics.warn("I18n", issue.detail, issue.code);
|
|
5230
|
+
}
|
|
5412
5231
|
var AgentRoleError, SETTING_ROWS;
|
|
5413
5232
|
var init_build = __esm({
|
|
5414
5233
|
"src/build.ts"() {
|
|
5415
5234
|
init_commands();
|
|
5235
|
+
init_events();
|
|
5416
5236
|
init_host_capabilities();
|
|
5237
|
+
init_host_marketplaces();
|
|
5417
5238
|
init_host_preset();
|
|
5418
5239
|
init_model_catalog();
|
|
5240
|
+
init_plugin_control();
|
|
5419
5241
|
init_plugin_mcp_servers();
|
|
5420
5242
|
init_role_write();
|
|
5421
5243
|
init_control();
|
|
@@ -5428,7 +5250,10 @@ var init_build = __esm({
|
|
|
5428
5250
|
init_telemetry();
|
|
5429
5251
|
init_tools();
|
|
5430
5252
|
init_user_shell();
|
|
5253
|
+
init_utility();
|
|
5254
|
+
init_workspace_files();
|
|
5431
5255
|
init_workspace();
|
|
5256
|
+
init_session_goal();
|
|
5432
5257
|
AgentRoleError = class extends Error {
|
|
5433
5258
|
constructor(message) {
|
|
5434
5259
|
super(message);
|
|
@@ -5442,15 +5267,6 @@ var init_build = __esm({
|
|
|
5442
5267
|
{ key: "fallback.providers" },
|
|
5443
5268
|
{ key: "contextLength" },
|
|
5444
5269
|
{ key: "maxTurns", scalar: "maxTurns" },
|
|
5445
|
-
// 2026-08-16 合并时收的一笔(`web/src/settings/context-memory.tsx` 文件头那张 📮)。
|
|
5446
|
-
// 数组顺序就是界面上的顺序,所以紧挨着 `contextLength` —— 那一块要拿这三行
|
|
5447
|
-
// 算「压缩在多少 token 上开始」,缺一行整块不画。
|
|
5448
|
-
//
|
|
5449
|
-
// ⚠️ **两行都不带 `scalar`**,不是漏了:带 `scalar` 的含义是「这个键在
|
|
5450
|
-
// `SettingsFileSchema` 白名单里、项目级压得回去」,而 `compression.*` 判过
|
|
5451
|
-
// 不进那张白名单(三条判据在 `core/src/config/sources.ts` 文件头)。
|
|
5452
|
-
// 📮 那条还提醒了:顺手写 `scalar: 'compression.enabled'` 编译不过,
|
|
5453
|
-
// `ScalarKey` 是从 `SCALAR_KEYS` 派生的闭集合。那道闸是对的。
|
|
5454
5270
|
{ key: "compression.enabled" },
|
|
5455
5271
|
{ key: "compression.threshold" },
|
|
5456
5272
|
{ key: "budget.maxCostUsd" },
|
|
@@ -5460,12 +5276,12 @@ var init_build = __esm({
|
|
|
5460
5276
|
];
|
|
5461
5277
|
}
|
|
5462
5278
|
});
|
|
5463
|
-
|
|
5464
5279
|
// src/index.ts
|
|
5465
5280
|
init_build();
|
|
5466
5281
|
init_host_capabilities();
|
|
5467
5282
|
init_host_preset();
|
|
5468
5283
|
init_workspace();
|
|
5284
|
+
init_workspace_files();
|
|
5469
5285
|
init_executor();
|
|
5470
5286
|
init_control();
|
|
5471
5287
|
init_agent_session();
|
|
@@ -5475,11 +5291,12 @@ init_services();
|
|
|
5475
5291
|
init_user_shell();
|
|
5476
5292
|
init_commands();
|
|
5477
5293
|
init_role_scope();
|
|
5294
|
+
init_host_marketplaces();
|
|
5478
5295
|
init_serialize();
|
|
5479
5296
|
init_tools();
|
|
5480
5297
|
init_model_catalog();
|
|
5298
|
+
init_utility();
|
|
5481
5299
|
init_plugin_mcp_servers();
|
|
5482
|
-
|
|
5483
5300
|
// src/mcp-admin.ts
|
|
5484
5301
|
init_build();
|
|
5485
5302
|
init_mcp_config_write();
|
|
@@ -5498,7 +5315,7 @@ async function listMcpServers(opts = {}) {
|
|
|
5498
5315
|
async function mcpLogin(name, opts = {}) {
|
|
5499
5316
|
const config = await requireServer(name, opts);
|
|
5500
5317
|
if (!config.url) {
|
|
5501
|
-
throw new Error(`MCP server "${name}
|
|
5318
|
+
throw new Error(t("boot.mcp_stdio_no_oauth", { name: `MCP server "${name}` }));
|
|
5502
5319
|
}
|
|
5503
5320
|
const result = await loginToMcpServer({
|
|
5504
5321
|
server: name,
|
|
@@ -5522,7 +5339,7 @@ async function probeMcpServer(name, opts = {}) {
|
|
|
5522
5339
|
const tools = status?.connected ? await registry.getAllTools() : [];
|
|
5523
5340
|
return {
|
|
5524
5341
|
connected: status?.connected ?? false,
|
|
5525
|
-
toolNames: tools.map((
|
|
5342
|
+
toolNames: tools.map((t23) => t23.name),
|
|
5526
5343
|
...status?.lastError ? { error: status.lastError } : {},
|
|
5527
5344
|
needsLogin: status?.needsLogin ?? false
|
|
5528
5345
|
};
|
|
@@ -5535,14 +5352,13 @@ async function requireServer(name, opts) {
|
|
|
5535
5352
|
const { servers, configPath: configPath2 } = await listMcpServers(opts);
|
|
5536
5353
|
const found = servers.find((s) => s.config.name === name);
|
|
5537
5354
|
if (!found) {
|
|
5538
|
-
const known = servers.map((s) => s.config.name).join(", ") || "
|
|
5539
|
-
throw new Error(
|
|
5355
|
+
const known = servers.map((s) => s.config.name).join(", ") || t("boot.mcp_list_empty");
|
|
5356
|
+
throw new Error(t("boot.mcp_no_such_server", { path: configPath2, name, known }));
|
|
5540
5357
|
}
|
|
5541
5358
|
return found.config;
|
|
5542
5359
|
}
|
|
5543
|
-
|
|
5544
5360
|
// src/index.ts
|
|
5545
5361
|
init_mcp_config_file();
|
|
5546
5362
|
init_mcp_config_apply();
|
|
5547
5363
|
|
|
5548
|
-
export { AgentRoleError, AgentSession, ApprovalRelay, HOST_CAPABILITIES_MODULE, HOST_PRESET_MODULE, QuestionRelay, SCHEDULE_EXIT_CODES, SessionStore, SessionWorkspaces, applyHostPreset, applyMcpConfig, buildModelCatalogControl, buildRuntime, buildServices, collectFileChanges, createScheduleControl, createUserActions, ensureSecretsReady, exitCodeFor, fireSchedule, listMcpServers, mcpConfigRevision, mcpLogin, mcpLogout, probeMcpServer, readMcpConfigFile, registerAskQuestion, registerLocalTools, registerMcpTools, serializeStream, splitDirList, toSerializable, wireCommands, wireHostCapabilities, wirePluginMcpServers, wireWorkspace, withModelScope, withRoleScope, withToolScope, writeMcpConfigFile, writeMcpServer };
|
|
5364
|
+
export { AgentRoleError, AgentSession, ApprovalRelay, HOST_CAPABILITIES_MODULE, HOST_MARKETPLACE_MODULE, HOST_PRESET_MODULE, QuestionRelay, SCHEDULE_EXIT_CODES, SessionStore, SessionWorkspaces, applyHostPreset, applyMcpConfig, buildModelCatalogControl, buildRuntime, buildServices, buildWorkspaceFilesControl, collectFileChanges, createScheduleControl, createUserActions, ensureSecretsReady, exitCodeFor, fireSchedule, listMcpServers, mcpConfigRevision, mcpLogin, mcpLogout, probeMcpServer, providerLabel, readMcpConfigFile, registerAskQuestion, registerLocalTools, registerMcpTools, serializeStream, splitDirList, toSerializable, utilityBudgetKey, wireCommands, wireHostCapabilities, wirePluginMcpServers, wireWorkspace, withModelScope, withRoleScope, withToolScope, writeMcpConfigFile, writeMcpServer };
|