@nextclaw/kernel 0.1.2-beta.6 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +869 -343
- package/dist/index.js +4281 -229
- package/dist/types/update-contract.types.d.ts +1 -1
- package/dist/update-manifest.types-DLWDvs_v.d.ts +27 -0
- package/package.json +14 -2
- package/dist/update-manifest.types-BAJqcsP5.d.ts +0 -59
package/dist/index.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { n as getUnsignedUpdateManifest, r as serializeUnsignedUpdateManifest, t as UpdateManifestReader } from "./update-manifest.types-C0qPrjGQ.js";
|
|
2
|
+
import { AgentRouteResolver, CONTEXT_COMPACTION_METADATA_KEY, ChannelManager, ChannelManager as ChannelManager$1, CommandRegistry, ContextBuilder, ContextCompactionService, ContextWindowBudgetService, CronService, CronTool, DEFAULT_SESSION_SEARCH_LIMIT, DisposableStore, EditFileTool, ExecTool, ExtensionToolAdapter, GatewayTool, InputBudgetPruner, LLMProvider, ListDirTool, LiteLLMProvider, MAX_SESSION_SEARCH_LIMIT, MemoryGetTool, MemorySearchTool, MessageBus, MessageTool, ProviderRegistry, ReadFileTool, RequestedSkillsMetadataReader, SessionManager, SessionRequestManager, SessionSearchManager, SessionsHistoryTool, SessionsListTool, Tool, ToolRegistry, WebFetchTool, WebSearchTool, WriteFileTool, buildCompressingCompactionCheckpoint, buildContextWindowSnapshot, buildMinimalSystemExecutionPrompt, buildReloadPlan, buildToolCatalogEntries, createAssistantStreamDeltaControlMessage, createAssistantStreamResetControlMessage, createToolExecutionContext, createTypingStopControlMessage, diffConfigPaths, ensureDir, expandHome, findEffectiveAgentProfile, getConfigPath, getDataDir, getSessionsPath, getWorkspacePath, loadConfig, parseAgentScopedSessionKey, parseThinkingLevel, readCompressedContextCompactionCheckpoint, readParentSessionId, readSessionProjectRoot, resolveConfigSecrets, resolveDefaultAgentProfileId, resolveProviderRuntime, resolveSessionWorkspacePath, resolveThinkingLevel, toDisposable } from "@nextclaw/core";
|
|
3
|
+
import { EventBus, Ingress, createAppEventKey, createTypedKey, eventKeys } from "@nextclaw/shared";
|
|
4
|
+
import { DefaultNcpAgentRuntime, LocalAssetStore, buildAssetContentPath, buildNcpUserContent, buildOpenAiFunctionTool } from "@nextclaw/ncp-agent-runtime";
|
|
5
|
+
import { NcpEventType, readAssistantReasoningNormalizationMode, readAssistantReasoningNormalizationModeFromMetadata, sanitizeAssistantReplyTags, writeAssistantReasoningNormalizationModeToMetadata } from "@nextclaw/ncp";
|
|
6
|
+
import { DefaultNcpAgentBackend, createAgentClientFromServer } from "@nextclaw/ncp-toolkit";
|
|
7
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
8
|
+
import { access, readFile } from "node:fs/promises";
|
|
9
|
+
import { appendFileSync, constants, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { BUILTIN_PROVIDER_PLUGINS } from "@nextclaw/runtime";
|
|
11
|
+
import { HttpRuntimeConfigResolver, HttpRuntimeNcpAgentRuntime } from "@nextclaw/nextclaw-ncp-runtime-http-client";
|
|
12
|
+
import { StdioRuntimeConfigResolver, StdioRuntimeNcpAgentRuntime, probeStdioRuntime } from "@nextclaw/nextclaw-ncp-runtime-stdio-client";
|
|
13
|
+
import { buildHermesAcpBridgeLaunchEnv, isHermesAcpRuntimeConfig } from "@nextclaw/nextclaw-hermes-acp-bridge";
|
|
14
|
+
import { McpRegistryService, McpServerLifecycleManager } from "@nextclaw/mcp";
|
|
15
|
+
import { McpNcpToolRegistryAdapter } from "@nextclaw/ncp-mcp";
|
|
16
|
+
import { getPluginChannelBindings, getPluginUiMetadataFromRegistry, mergePluginConfigView, resolvePluginChannelMessageToolHints, toPluginConfigView } from "@nextclaw/openclaw-compat";
|
|
2
17
|
//#region src/managers/agent.manager.ts
|
|
3
18
|
var AgentManager = class {
|
|
4
19
|
listAgents = () => {
|
|
@@ -30,114 +45,3896 @@ var AgentManager = class {
|
|
|
30
45
|
};
|
|
31
46
|
};
|
|
32
47
|
//#endregion
|
|
33
|
-
//#region src/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
//#region src/features/narp-runtime/services/builtin-narp-runtime-registration.service.ts
|
|
49
|
+
const NARP_API_MODE_HEADER = "x-nextclaw-narp-api-mode";
|
|
50
|
+
const NARP_HTTP_RUNTIME_KIND = "narp-http";
|
|
51
|
+
const NARP_STDIO_RUNTIME_KIND = "narp-stdio";
|
|
52
|
+
const BUILTIN_PROVIDER_REGISTRY = new ProviderRegistry(BUILTIN_PROVIDER_PLUGINS);
|
|
53
|
+
function readRecord$1(value) {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function readString$2(value) {
|
|
58
|
+
if (typeof value !== "string") return;
|
|
59
|
+
return value.trim() || void 0;
|
|
60
|
+
}
|
|
61
|
+
function resolveEntryIcon(entry) {
|
|
62
|
+
return entry.icon ?? null;
|
|
63
|
+
}
|
|
64
|
+
function resolveRequestedModel(params) {
|
|
65
|
+
const { configuredModel, defaultModel, input, sessionMetadata } = params;
|
|
66
|
+
const runMetadata = readRecord$1(input.metadata);
|
|
67
|
+
return readString$2(runMetadata?.preferred_model) ?? readString$2(runMetadata?.preferredModel) ?? readString$2(runMetadata?.model) ?? readString$2(sessionMetadata.preferred_model) ?? readString$2(sessionMetadata.preferredModel) ?? readString$2(sessionMetadata.model) ?? configuredModel ?? defaultModel;
|
|
68
|
+
}
|
|
69
|
+
function resolveProviderApiMode(resolution) {
|
|
70
|
+
const wireApi = resolution.provider?.wireApi?.trim().toLowerCase();
|
|
71
|
+
if (wireApi === "responses") return "codex_responses";
|
|
72
|
+
if (wireApi === "chat") return "chat_completions";
|
|
73
|
+
const providerName = resolution.providerName?.trim().toLowerCase();
|
|
74
|
+
const apiBase = resolution.apiBase?.trim().toLowerCase() ?? "";
|
|
75
|
+
if (providerName === "anthropic" || apiBase.includes("anthropic.com")) return "anthropic_messages";
|
|
76
|
+
return "chat_completions";
|
|
77
|
+
}
|
|
78
|
+
function buildProviderRoute(params) {
|
|
79
|
+
const model = resolveRequestedModel(params);
|
|
80
|
+
if (!model) return;
|
|
81
|
+
const resolution = resolveProviderRuntime(params.config, model);
|
|
82
|
+
if (!resolution.provider) return;
|
|
83
|
+
const apiBase = resolution.apiBase ?? (resolution.providerName ? BUILTIN_PROVIDER_REGISTRY.findProviderByName(resolution.providerName)?.defaultApiBase ?? null : null);
|
|
84
|
+
return {
|
|
85
|
+
model: resolution.providerLocalModel,
|
|
86
|
+
apiKey: resolution.apiKey,
|
|
87
|
+
apiBase,
|
|
88
|
+
headers: {
|
|
89
|
+
...resolution.provider.extraHeaders ?? {},
|
|
90
|
+
[NARP_API_MODE_HEADER]: resolveProviderApiMode(resolution)
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
var BuiltinHttpRuntimeSessionTypeService = class {
|
|
95
|
+
pendingDescribeByMode = /* @__PURE__ */ new Map();
|
|
96
|
+
constructor(entry, defaultModel) {
|
|
97
|
+
this.entry = entry;
|
|
98
|
+
this.defaultModel = defaultModel;
|
|
99
|
+
}
|
|
100
|
+
describe = async (describeParams) => {
|
|
101
|
+
const describeMode = describeParams?.describeMode === "probe" ? "probe" : "observation";
|
|
102
|
+
const pending = this.pendingDescribeByMode.get(describeMode);
|
|
103
|
+
if (pending) return await pending;
|
|
104
|
+
const nextDescribe = this.describeInternal();
|
|
105
|
+
this.pendingDescribeByMode.set(describeMode, nextDescribe);
|
|
106
|
+
try {
|
|
107
|
+
return await nextDescribe;
|
|
108
|
+
} finally {
|
|
109
|
+
this.pendingDescribeByMode.delete(describeMode);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
describeInternal = async () => {
|
|
113
|
+
const config = new HttpRuntimeConfigResolver(this.entry.config ?? {}).resolve({ defaultModel: this.defaultModel });
|
|
114
|
+
if (!config.baseUrl) return {
|
|
115
|
+
icon: resolveEntryIcon(this.entry),
|
|
116
|
+
ready: false,
|
|
117
|
+
reason: "base_url_missing",
|
|
118
|
+
reasonMessage: "Configure the runtime entry baseUrl before starting an HTTP runtime session.",
|
|
119
|
+
recommendedModel: config.recommendedModel ?? null,
|
|
120
|
+
cta: {
|
|
121
|
+
kind: "settings",
|
|
122
|
+
label: "Configure HTTP Runtime"
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
if (!((config.capabilityProbe ?? true) && Boolean(config.healthcheckUrl))) return {
|
|
126
|
+
icon: resolveEntryIcon(this.entry),
|
|
127
|
+
ready: true,
|
|
128
|
+
reason: null,
|
|
129
|
+
reasonMessage: null,
|
|
130
|
+
recommendedModel: config.recommendedModel ?? null,
|
|
131
|
+
...config.supportedModels ? { supportedModels: config.supportedModels } : {},
|
|
132
|
+
cta: null
|
|
133
|
+
};
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
const timeoutMs = config.healthcheckTimeoutMs ?? 3e3;
|
|
136
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetch(config.healthcheckUrl ?? "", {
|
|
139
|
+
method: "GET",
|
|
140
|
+
headers: config.headers,
|
|
141
|
+
signal: controller.signal
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok) return {
|
|
144
|
+
icon: resolveEntryIcon(this.entry),
|
|
145
|
+
ready: false,
|
|
146
|
+
reason: "healthcheck_failed",
|
|
147
|
+
reasonMessage: `HTTP runtime healthcheck returned HTTP ${response.status}.`,
|
|
148
|
+
recommendedModel: config.recommendedModel ?? null,
|
|
149
|
+
...config.supportedModels ? { supportedModels: config.supportedModels } : {},
|
|
150
|
+
cta: null
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
icon: resolveEntryIcon(this.entry),
|
|
154
|
+
ready: true,
|
|
155
|
+
reason: null,
|
|
156
|
+
reasonMessage: null,
|
|
157
|
+
recommendedModel: config.recommendedModel ?? null,
|
|
158
|
+
...config.supportedModels ? { supportedModels: config.supportedModels } : {},
|
|
159
|
+
cta: null
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return {
|
|
163
|
+
icon: resolveEntryIcon(this.entry),
|
|
164
|
+
ready: false,
|
|
165
|
+
reason: "healthcheck_unreachable",
|
|
166
|
+
reasonMessage: error instanceof Error ? error.message : "Failed to reach the configured HTTP runtime healthcheck.",
|
|
167
|
+
recommendedModel: config.recommendedModel ?? null,
|
|
168
|
+
...config.supportedModels ? { supportedModels: config.supportedModels } : {},
|
|
169
|
+
cta: null
|
|
170
|
+
};
|
|
171
|
+
} finally {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
var BuiltinStdioRuntimeSessionTypeService = class {
|
|
177
|
+
pendingDescribeByMode = /* @__PURE__ */ new Map();
|
|
178
|
+
constructor(entry) {
|
|
179
|
+
this.entry = entry;
|
|
180
|
+
}
|
|
181
|
+
describe = async (describeParams) => {
|
|
182
|
+
const describeMode = describeParams?.describeMode === "probe" ? "probe" : "observation";
|
|
183
|
+
const pending = this.pendingDescribeByMode.get(describeMode);
|
|
184
|
+
if (pending) return await pending;
|
|
185
|
+
const nextDescribe = this.describeInternal(describeMode);
|
|
186
|
+
this.pendingDescribeByMode.set(describeMode, nextDescribe);
|
|
187
|
+
try {
|
|
188
|
+
return await nextDescribe;
|
|
189
|
+
} finally {
|
|
190
|
+
this.pendingDescribeByMode.delete(describeMode);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
describeInternal = async (describeMode) => {
|
|
194
|
+
const resolver = new StdioRuntimeConfigResolver(this.entry.config ?? {});
|
|
195
|
+
try {
|
|
196
|
+
const config = applyBuiltinStdioBridgeEnv(resolver.resolve(), { useProbeRoute: describeMode === "probe" });
|
|
197
|
+
if (!await resolveExecutablePath(config.command)) return {
|
|
198
|
+
icon: resolveEntryIcon(this.entry),
|
|
199
|
+
ready: false,
|
|
200
|
+
reason: "command_missing",
|
|
201
|
+
reasonMessage: `Configured stdio command "${config.command}" is not available. Update the runtime entry command or install the required launcher first.`,
|
|
202
|
+
cta: {
|
|
203
|
+
kind: "settings",
|
|
204
|
+
label: "Configure Stdio Runtime"
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
if (describeMode === "probe") try {
|
|
208
|
+
await probeStdioRuntime(config);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return {
|
|
211
|
+
icon: resolveEntryIcon(this.entry),
|
|
212
|
+
ready: false,
|
|
213
|
+
reason: "probe_failed",
|
|
214
|
+
reasonMessage: error instanceof Error ? error.message : "Configured stdio runtime could not complete the ACP probe.",
|
|
215
|
+
cta: {
|
|
216
|
+
kind: "settings",
|
|
217
|
+
label: "Repair Stdio Runtime"
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
icon: resolveEntryIcon(this.entry),
|
|
223
|
+
ready: true,
|
|
224
|
+
reason: null,
|
|
225
|
+
reasonMessage: null,
|
|
226
|
+
cta: null
|
|
227
|
+
};
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return {
|
|
230
|
+
icon: resolveEntryIcon(this.entry),
|
|
231
|
+
ready: false,
|
|
232
|
+
reason: "command_missing",
|
|
233
|
+
reasonMessage: error instanceof Error ? error.message : "Configure a stdio command before starting this runtime.",
|
|
234
|
+
cta: {
|
|
235
|
+
kind: "settings",
|
|
236
|
+
label: "Configure Stdio Runtime"
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
async function resolveExecutablePath(command) {
|
|
243
|
+
if (command.includes("/") || command.includes("\\")) return await isExecutable(command) ? command : null;
|
|
244
|
+
const searchPath = process.env.PATH ?? "";
|
|
245
|
+
for (const directory of searchPath.split(delimiter)) {
|
|
246
|
+
const trimmed = directory.trim();
|
|
247
|
+
if (!trimmed) continue;
|
|
248
|
+
const candidate = `${trimmed}/${command}`;
|
|
249
|
+
if (await isExecutable(candidate)) return candidate;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
async function isExecutable(filePath) {
|
|
254
|
+
try {
|
|
255
|
+
await access(filePath, constants.X_OK);
|
|
256
|
+
return true;
|
|
257
|
+
} catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
var BuiltinNarpRuntimeRegistrationService = class {
|
|
262
|
+
constructor(getConfig) {
|
|
263
|
+
this.getConfig = getConfig;
|
|
264
|
+
}
|
|
265
|
+
registerInto = (runtimeRegistry) => {
|
|
266
|
+
return [runtimeRegistry.register({
|
|
267
|
+
kind: NARP_HTTP_RUNTIME_KIND,
|
|
268
|
+
label: "NARP HTTP",
|
|
269
|
+
createRuntime: this.createUnavailableRuntime,
|
|
270
|
+
createRuntimeForEntry: ({ entry, runtimeParams }) => this.createHttpRuntime(entry, runtimeParams),
|
|
271
|
+
describeSessionTypeForEntry: ({ entry, describeParams }) => new BuiltinHttpRuntimeSessionTypeService(entry, this.getConfig().agents.defaults.model).describe(describeParams)
|
|
272
|
+
}), runtimeRegistry.register({
|
|
273
|
+
kind: NARP_STDIO_RUNTIME_KIND,
|
|
274
|
+
label: "NARP Stdio",
|
|
275
|
+
createRuntime: this.createUnavailableRuntime,
|
|
276
|
+
createRuntimeForEntry: ({ entry, runtimeParams }) => this.createStdioRuntime(entry, runtimeParams),
|
|
277
|
+
describeSessionTypeForEntry: ({ entry, describeParams }) => new BuiltinStdioRuntimeSessionTypeService(entry).describe(describeParams)
|
|
278
|
+
})];
|
|
279
|
+
};
|
|
280
|
+
createUnavailableRuntime = () => {
|
|
281
|
+
throw new Error("[narp] runtime entry is required before creating this runtime");
|
|
282
|
+
};
|
|
283
|
+
createHttpRuntime = (entry, runtimeParams) => {
|
|
284
|
+
const config = readRecord$1(entry.config) ?? {};
|
|
285
|
+
const resolver = new HttpRuntimeConfigResolver(config);
|
|
286
|
+
const resolvedConfig = resolver.resolve({ defaultModel: this.getConfig().agents.defaults.model });
|
|
287
|
+
return new HttpRuntimeNcpAgentRuntime({
|
|
288
|
+
baseUrl: resolver.requireBaseUrl(),
|
|
289
|
+
...resolvedConfig.basePath ? { basePath: resolvedConfig.basePath } : {},
|
|
290
|
+
...resolvedConfig.endpointId ? { endpointId: resolvedConfig.endpointId } : {},
|
|
291
|
+
...resolvedConfig.headers ? { headers: resolvedConfig.headers } : {},
|
|
292
|
+
stateManager: runtimeParams.stateManager,
|
|
293
|
+
resolveTools: runtimeParams.resolveTools,
|
|
294
|
+
resolveProviderRoute: (input) => buildProviderRoute({
|
|
295
|
+
config: this.getConfig(),
|
|
296
|
+
input,
|
|
297
|
+
sessionMetadata: runtimeParams.sessionMetadata,
|
|
298
|
+
defaultModel: this.getConfig().agents.defaults.model,
|
|
299
|
+
configuredModel: readString$2(config.model)
|
|
300
|
+
})
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
createStdioRuntime = (entry, runtimeParams) => {
|
|
304
|
+
const config = readRecord$1(entry.config) ?? {};
|
|
305
|
+
return new StdioRuntimeNcpAgentRuntime({
|
|
306
|
+
...applyBuiltinStdioBridgeEnv(new StdioRuntimeConfigResolver(config).resolve()),
|
|
307
|
+
sessionId: runtimeParams.sessionId,
|
|
308
|
+
stateManager: runtimeParams.stateManager,
|
|
309
|
+
resolveTools: runtimeParams.resolveTools,
|
|
310
|
+
resolveProviderRoute: (input) => buildProviderRoute({
|
|
311
|
+
config: this.getConfig(),
|
|
312
|
+
input,
|
|
313
|
+
sessionMetadata: runtimeParams.sessionMetadata,
|
|
314
|
+
defaultModel: this.getConfig().agents.defaults.model,
|
|
315
|
+
configuredModel: readString$2(config.model)
|
|
316
|
+
})
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
function applyBuiltinStdioBridgeEnv(config, params) {
|
|
321
|
+
if (!isHermesAcpRuntimeConfig(config)) return config;
|
|
322
|
+
return {
|
|
323
|
+
...config,
|
|
324
|
+
env: buildHermesAcpBridgeLaunchEnv({
|
|
325
|
+
command: config.command,
|
|
326
|
+
args: config.args,
|
|
327
|
+
baseEnv: config.env,
|
|
328
|
+
useProbeRoute: params?.useProbeRoute
|
|
329
|
+
})
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/features/runtime-registry/controllers/plugin-runtime-registration.controller.ts
|
|
334
|
+
const RESERVED_BUILTIN_RUNTIME_KINDS = new Set([NARP_HTTP_RUNTIME_KIND, NARP_STDIO_RUNTIME_KIND]);
|
|
335
|
+
function buildPluginRuntimeSnapshotKey(extensionRegistry) {
|
|
336
|
+
return (extensionRegistry?.ncpAgentRuntimes ?? []).map((registration) => [
|
|
337
|
+
registration.pluginId,
|
|
338
|
+
registration.kind,
|
|
339
|
+
registration.label,
|
|
340
|
+
registration.source
|
|
341
|
+
].join(":")).join("|");
|
|
342
|
+
}
|
|
343
|
+
var PluginRuntimeRegistrationController = class {
|
|
344
|
+
pluginRuntimeScopes = /* @__PURE__ */ new Map();
|
|
345
|
+
pluginRuntimeSnapshotKey = "";
|
|
346
|
+
constructor(runtimeRegistry, extensions) {
|
|
347
|
+
this.runtimeRegistry = runtimeRegistry;
|
|
348
|
+
this.extensions = extensions;
|
|
349
|
+
}
|
|
350
|
+
refreshPluginRuntimeRegistrations = () => {
|
|
351
|
+
this.syncPluginRuntimeRegistrations(this.resolveActiveExtensionRegistry());
|
|
352
|
+
};
|
|
353
|
+
dispose = () => {
|
|
354
|
+
for (const scope of this.pluginRuntimeScopes.values()) scope.dispose();
|
|
355
|
+
this.pluginRuntimeScopes.clear();
|
|
356
|
+
this.pluginRuntimeSnapshotKey = "";
|
|
357
|
+
};
|
|
358
|
+
syncPluginRuntimeRegistrations = (extensionRegistry) => {
|
|
359
|
+
const nextSnapshotKey = buildPluginRuntimeSnapshotKey(extensionRegistry);
|
|
360
|
+
if (nextSnapshotKey === this.pluginRuntimeSnapshotKey) return;
|
|
361
|
+
this.pluginRuntimeSnapshotKey = nextSnapshotKey;
|
|
362
|
+
for (const scope of this.pluginRuntimeScopes.values()) scope.dispose();
|
|
363
|
+
this.pluginRuntimeScopes.clear();
|
|
364
|
+
for (const registration of extensionRegistry?.ncpAgentRuntimes ?? []) {
|
|
365
|
+
if (RESERVED_BUILTIN_RUNTIME_KINDS.has(registration.kind.trim().toLowerCase())) continue;
|
|
366
|
+
const pluginId = registration.pluginId.trim() || registration.kind;
|
|
367
|
+
const scope = this.pluginRuntimeScopes.get(pluginId) ?? new DisposableStore();
|
|
368
|
+
this.pluginRuntimeScopes.set(pluginId, scope);
|
|
369
|
+
const createRuntimeForEntry = registration.createRuntimeForEntry;
|
|
370
|
+
scope.add(this.runtimeRegistry.register({
|
|
371
|
+
kind: registration.kind,
|
|
372
|
+
label: registration.label,
|
|
373
|
+
createRuntime: registration.createRuntime,
|
|
374
|
+
createRuntimeForEntry: createRuntimeForEntry ? ({ entry, runtimeParams }) => createRuntimeForEntry({
|
|
375
|
+
entry,
|
|
376
|
+
runtimeParams: {
|
|
377
|
+
...runtimeParams,
|
|
378
|
+
sessionMetadata: {
|
|
379
|
+
...runtimeParams.sessionMetadata,
|
|
380
|
+
runtime_type: entry.type
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}) : void 0,
|
|
384
|
+
describeSessionType: registration.describeSessionType,
|
|
385
|
+
describeSessionTypeForEntry: registration.describeSessionTypeForEntry
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
resolveActiveExtensionRegistry = () => this.extensions.getExtensionRegistry();
|
|
390
|
+
};
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/features/runtime-registry/services/agent-runtime-registry.service.ts
|
|
393
|
+
const DEFAULT_AGENT_RUNTIME_ENTRY_ID = "native";
|
|
394
|
+
function normalizeIdentifier(value) {
|
|
395
|
+
if (typeof value !== "string") return null;
|
|
396
|
+
const normalized = value.trim().toLowerCase();
|
|
397
|
+
return normalized.length > 0 ? normalized : null;
|
|
398
|
+
}
|
|
399
|
+
function cloneRuntimeEntry(entry) {
|
|
400
|
+
return {
|
|
401
|
+
id: entry.id,
|
|
402
|
+
label: entry.label,
|
|
403
|
+
...entry.icon ? { icon: { ...entry.icon } } : {},
|
|
404
|
+
type: entry.type,
|
|
405
|
+
enabled: entry.enabled !== false,
|
|
406
|
+
config: entry.config ? { ...entry.config } : {}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function normalizeAgentRuntimeSessionTypeIcon(value) {
|
|
410
|
+
if (typeof value === "string") {
|
|
411
|
+
const src = value.trim();
|
|
412
|
+
return src ? {
|
|
413
|
+
kind: "image",
|
|
414
|
+
src
|
|
415
|
+
} : null;
|
|
416
|
+
}
|
|
417
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
418
|
+
const iconRecord = value;
|
|
419
|
+
const src = typeof iconRecord.src === "string" ? iconRecord.src.trim() : "";
|
|
420
|
+
if (!src) return null;
|
|
421
|
+
const alt = typeof iconRecord.alt === "string" && iconRecord.alt.trim().length > 0 ? iconRecord.alt.trim() : null;
|
|
422
|
+
return {
|
|
423
|
+
kind: "image",
|
|
424
|
+
src,
|
|
425
|
+
...alt ? { alt } : {}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const BUILTIN_RUNTIME_PRESENTATION = { hermes: {
|
|
429
|
+
label: "Hermes",
|
|
430
|
+
icon: {
|
|
431
|
+
kind: "image",
|
|
432
|
+
src: "app://runtime-icons/hermes-agent.png",
|
|
433
|
+
alt: "Hermes"
|
|
434
|
+
}
|
|
435
|
+
} };
|
|
436
|
+
function readString$1(value) {
|
|
437
|
+
if (typeof value !== "string") return;
|
|
438
|
+
return value.trim() || void 0;
|
|
439
|
+
}
|
|
440
|
+
function readRecord(value) {
|
|
441
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
442
|
+
return value;
|
|
443
|
+
}
|
|
444
|
+
function resolveBuiltinRuntimePresentation(value) {
|
|
445
|
+
const normalized = readString$1(value)?.toLowerCase();
|
|
446
|
+
if (!normalized) return null;
|
|
447
|
+
if (!(normalized in BUILTIN_RUNTIME_PRESENTATION)) return null;
|
|
448
|
+
return BUILTIN_RUNTIME_PRESENTATION[normalized] ?? null;
|
|
449
|
+
}
|
|
450
|
+
function buildNativeRuntimeEntry(config) {
|
|
451
|
+
const nativeRuntimeConfig = readRecord(config.ui?.ncp?.runtimes?.native) ?? {};
|
|
452
|
+
return {
|
|
453
|
+
id: DEFAULT_AGENT_RUNTIME_ENTRY_ID,
|
|
454
|
+
label: "Native",
|
|
455
|
+
type: DEFAULT_AGENT_RUNTIME_ENTRY_ID,
|
|
456
|
+
enabled: nativeRuntimeConfig.enabled !== false,
|
|
457
|
+
config: nativeRuntimeConfig
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function resolveAgentRuntimeEntries(params) {
|
|
461
|
+
const entries = /* @__PURE__ */ new Map();
|
|
462
|
+
entries.set(DEFAULT_AGENT_RUNTIME_ENTRY_ID, buildNativeRuntimeEntry(params.config));
|
|
463
|
+
for (const [rawId, rawEntry] of Object.entries(params.config.agents.runtimes.entries ?? {})) {
|
|
464
|
+
const id = rawId.trim().toLowerCase();
|
|
465
|
+
const type = readString$1(rawEntry.type)?.toLowerCase();
|
|
466
|
+
if (!id || !type) continue;
|
|
467
|
+
const explicitIcon = normalizeAgentRuntimeSessionTypeIcon(rawEntry.icon);
|
|
468
|
+
const builtinPresentation = resolveBuiltinRuntimePresentation(id) ?? resolveBuiltinRuntimePresentation(type);
|
|
469
|
+
entries.set(id, {
|
|
470
|
+
id,
|
|
471
|
+
label: readString$1(rawEntry.label) ?? builtinPresentation?.label ?? id,
|
|
472
|
+
...explicitIcon ?? builtinPresentation?.icon ? { icon: explicitIcon ?? builtinPresentation?.icon } : {},
|
|
473
|
+
type,
|
|
474
|
+
enabled: rawEntry.enabled !== false,
|
|
475
|
+
config: rawEntry.config ? { ...rawEntry.config } : {}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
defaultEntryId: entries.has("native") ? DEFAULT_AGENT_RUNTIME_ENTRY_ID : [...entries.keys()][0] ?? "native",
|
|
480
|
+
entries: [...entries.values()]
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function readRequestedRuntimeEntryId(sessionMetadata) {
|
|
484
|
+
return normalizeIdentifier(sessionMetadata.runtime) ?? normalizeIdentifier(sessionMetadata.session_type) ?? normalizeIdentifier(sessionMetadata.sessionType) ?? null;
|
|
485
|
+
}
|
|
486
|
+
var AgentRuntimeRegistry = class {
|
|
487
|
+
providers = /* @__PURE__ */ new Map();
|
|
488
|
+
entries = /* @__PURE__ */ new Map();
|
|
489
|
+
defaultEntryId = DEFAULT_AGENT_RUNTIME_ENTRY_ID;
|
|
490
|
+
register = (registration) => {
|
|
491
|
+
const normalizedKind = normalizeIdentifier(registration.kind);
|
|
492
|
+
if (!normalizedKind) throw new Error("agent runtime kind must be a non-empty string");
|
|
493
|
+
const token = Symbol(normalizedKind);
|
|
494
|
+
this.providers.set(normalizedKind, {
|
|
495
|
+
...registration,
|
|
496
|
+
kind: normalizedKind,
|
|
497
|
+
token
|
|
498
|
+
});
|
|
499
|
+
return toDisposable(() => {
|
|
500
|
+
const current = this.providers.get(normalizedKind);
|
|
501
|
+
if (!current || current.token !== token) return;
|
|
502
|
+
this.providers.delete(normalizedKind);
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
applyEntries = (params) => {
|
|
506
|
+
const nextEntries = /* @__PURE__ */ new Map();
|
|
507
|
+
for (const rawEntry of params.entries) {
|
|
508
|
+
const id = normalizeIdentifier(rawEntry.id);
|
|
509
|
+
const type = normalizeIdentifier(rawEntry.type);
|
|
510
|
+
const label = rawEntry.label?.trim() ?? "";
|
|
511
|
+
if (!id || !type || !label) continue;
|
|
512
|
+
nextEntries.set(id, cloneRuntimeEntry({
|
|
513
|
+
...rawEntry,
|
|
514
|
+
id,
|
|
515
|
+
type,
|
|
516
|
+
label
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
this.entries = nextEntries;
|
|
520
|
+
this.defaultEntryId = normalizeIdentifier(params.defaultEntryId) ?? (nextEntries.has("native") ? "native" : [...nextEntries.keys()][0] ?? "native");
|
|
521
|
+
};
|
|
522
|
+
listProviderKinds = () => {
|
|
523
|
+
return [...this.providers.keys()];
|
|
524
|
+
};
|
|
525
|
+
createRuntime = (params) => {
|
|
526
|
+
const requestedEntryId = readRequestedRuntimeEntryId(params.sessionMetadata) ?? this.defaultEntryId;
|
|
527
|
+
const entry = this.entries.get(requestedEntryId);
|
|
528
|
+
if (!entry || entry.enabled === false) throw new Error(`ncp runtime unavailable: ${requestedEntryId}`);
|
|
529
|
+
const provider = this.providers.get(entry.type);
|
|
530
|
+
if (!provider) throw new Error(`ncp runtime provider unavailable: ${entry.type}`);
|
|
531
|
+
const nextSessionMetadata = {
|
|
532
|
+
...params.sessionMetadata,
|
|
533
|
+
runtime: entry.id,
|
|
534
|
+
session_type: entry.id,
|
|
535
|
+
runtime_type: entry.type
|
|
536
|
+
};
|
|
537
|
+
params.setSessionMetadata(nextSessionMetadata);
|
|
538
|
+
if (provider.createRuntimeForEntry) return provider.createRuntimeForEntry({
|
|
539
|
+
entry: cloneRuntimeEntry(entry),
|
|
540
|
+
runtimeParams: {
|
|
541
|
+
...params,
|
|
542
|
+
sessionMetadata: nextSessionMetadata
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
return provider.createRuntime({
|
|
546
|
+
...params,
|
|
547
|
+
sessionMetadata: nextSessionMetadata
|
|
548
|
+
});
|
|
549
|
+
};
|
|
550
|
+
listSessionTypes = async (params) => {
|
|
551
|
+
const options = await Promise.all([...this.entries.values()].filter((entry) => entry.enabled !== false).map(async (entry) => {
|
|
552
|
+
const provider = this.providers.get(entry.type);
|
|
553
|
+
if (!provider) return {
|
|
554
|
+
value: entry.id,
|
|
555
|
+
label: entry.label,
|
|
556
|
+
icon: entry.icon ?? null,
|
|
557
|
+
ready: false,
|
|
558
|
+
reason: "runtime_provider_unavailable",
|
|
559
|
+
reasonMessage: `Runtime provider unavailable for type "${entry.type}".`,
|
|
560
|
+
recommendedModel: null,
|
|
561
|
+
cta: {
|
|
562
|
+
kind: "settings",
|
|
563
|
+
label: "Configure Runtime"
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
const descriptor = provider.describeSessionTypeForEntry ? await provider.describeSessionTypeForEntry({
|
|
567
|
+
entry: cloneRuntimeEntry(entry),
|
|
568
|
+
describeParams: params
|
|
569
|
+
}) : await provider.describeSessionType?.(params);
|
|
570
|
+
return {
|
|
571
|
+
value: entry.id,
|
|
572
|
+
label: entry.label,
|
|
573
|
+
icon: descriptor?.icon ?? entry.icon ?? null,
|
|
574
|
+
ready: descriptor?.ready ?? true,
|
|
575
|
+
reason: descriptor?.reason ?? null,
|
|
576
|
+
reasonMessage: descriptor?.reasonMessage ?? null,
|
|
577
|
+
recommendedModel: descriptor?.recommendedModel ?? null,
|
|
578
|
+
cta: descriptor?.cta ?? null,
|
|
579
|
+
...descriptor?.supportedModels ? { supportedModels: descriptor.supportedModels } : {}
|
|
580
|
+
};
|
|
581
|
+
}));
|
|
582
|
+
return {
|
|
583
|
+
defaultType: this.defaultEntryId,
|
|
584
|
+
options: options.sort((left, right) => {
|
|
585
|
+
if (left.value === this.defaultEntryId) return -1;
|
|
586
|
+
if (right.value === this.defaultEntryId) return 1;
|
|
587
|
+
return left.value.localeCompare(right.value);
|
|
588
|
+
})
|
|
589
|
+
};
|
|
590
|
+
};
|
|
591
|
+
};
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/features/ncp-dispatch/utils/agent-runtime-handle.utils.ts
|
|
594
|
+
function createAgentRuntimeHandle(params) {
|
|
595
|
+
const { backend, runtimeRegistry, refreshPluginRuntimeRegistrations, refreshConfiguredRuntimeEntries, applyMcpConfig, dispose, assetStore } = params;
|
|
596
|
+
return {
|
|
597
|
+
basePath: "/api/ncp/agent",
|
|
598
|
+
agentClientEndpoint: createAgentClientFromServer(backend),
|
|
599
|
+
streamProvider: backend,
|
|
600
|
+
runApi: backend,
|
|
601
|
+
sessionApi: backend,
|
|
602
|
+
listSessionTypes: (describeParams) => {
|
|
603
|
+
refreshPluginRuntimeRegistrations();
|
|
604
|
+
refreshConfiguredRuntimeEntries();
|
|
605
|
+
return runtimeRegistry.listSessionTypes(describeParams);
|
|
606
|
+
},
|
|
607
|
+
assetApi: {
|
|
608
|
+
put: (input) => assetStore.putBytes({
|
|
609
|
+
fileName: input.fileName,
|
|
610
|
+
mimeType: input.mimeType,
|
|
611
|
+
bytes: input.bytes,
|
|
612
|
+
createdAt: input.createdAt
|
|
613
|
+
}),
|
|
614
|
+
stat: (uri) => assetStore.statRecord(uri),
|
|
615
|
+
resolveContentPath: (uri) => assetStore.resolveContentPath(uri)
|
|
616
|
+
},
|
|
617
|
+
applyMcpConfig,
|
|
618
|
+
dispose
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
//#endregion
|
|
622
|
+
//#region src/utils/ncp-message-bridge.utils.ts
|
|
623
|
+
function normalizeString(value) {
|
|
624
|
+
if (typeof value !== "string") return null;
|
|
625
|
+
const trimmed = value.trim();
|
|
626
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
627
|
+
}
|
|
628
|
+
function isRecord$3(value) {
|
|
629
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
630
|
+
}
|
|
631
|
+
function cloneMetadata(value) {
|
|
632
|
+
return isRecord$3(value) ? structuredClone(value) : void 0;
|
|
633
|
+
}
|
|
634
|
+
function readStringArray(value) {
|
|
635
|
+
if (!Array.isArray(value)) return null;
|
|
636
|
+
const deduped = /* @__PURE__ */ new Set();
|
|
637
|
+
for (const item of value) {
|
|
638
|
+
const normalized = normalizeString(item);
|
|
639
|
+
if (normalized) deduped.add(normalized);
|
|
640
|
+
}
|
|
641
|
+
return [...deduped];
|
|
642
|
+
}
|
|
643
|
+
function mergeSessionMetadata(currentMetadata, inputMetadata) {
|
|
644
|
+
if (!inputMetadata) return currentMetadata;
|
|
645
|
+
const nextMetadata = {
|
|
646
|
+
...currentMetadata,
|
|
647
|
+
...structuredClone(inputMetadata)
|
|
648
|
+
};
|
|
649
|
+
const model = normalizeString(inputMetadata.model) ?? normalizeString(inputMetadata.preferred_model) ?? normalizeString(inputMetadata.preferredModel);
|
|
650
|
+
if (model) {
|
|
651
|
+
nextMetadata.model = model;
|
|
652
|
+
nextMetadata.preferred_model = model;
|
|
653
|
+
}
|
|
654
|
+
const thinking = normalizeString(inputMetadata.thinking) ?? normalizeString(inputMetadata.preferred_thinking) ?? normalizeString(inputMetadata.thinking_level) ?? normalizeString(inputMetadata.thinkingLevel);
|
|
655
|
+
if (thinking) {
|
|
656
|
+
nextMetadata.thinking = thinking;
|
|
657
|
+
nextMetadata.preferred_thinking = thinking;
|
|
658
|
+
}
|
|
659
|
+
const sessionType = normalizeString(inputMetadata.session_type) ?? normalizeString(inputMetadata.sessionType);
|
|
660
|
+
if (sessionType) nextMetadata.session_type = sessionType;
|
|
661
|
+
const label = normalizeString(inputMetadata.label) ?? normalizeString(inputMetadata.session_label);
|
|
662
|
+
if (label) nextMetadata.label = label;
|
|
663
|
+
const requestedSkills = readStringArray(inputMetadata.requested_skills) ?? readStringArray(inputMetadata.requestedSkills);
|
|
664
|
+
if (requestedSkills) nextMetadata.requested_skills = requestedSkills;
|
|
665
|
+
return nextMetadata;
|
|
666
|
+
}
|
|
667
|
+
function extractMessageMetadata(messages) {
|
|
668
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
669
|
+
const message = messages[index];
|
|
670
|
+
if (message?.role !== "user") continue;
|
|
671
|
+
const metadata = cloneMetadata(message.metadata);
|
|
672
|
+
if (metadata) return metadata;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function ensureIsoTimestamp(value, fallback) {
|
|
676
|
+
if (typeof value !== "string") return fallback;
|
|
677
|
+
const timestamp = Date.parse(value);
|
|
678
|
+
if (!Number.isFinite(timestamp)) return fallback;
|
|
679
|
+
return new Date(timestamp).toISOString();
|
|
680
|
+
}
|
|
681
|
+
function serializeToolArgs(args) {
|
|
682
|
+
if (typeof args === "string") return args;
|
|
683
|
+
return JSON.stringify(args ?? {});
|
|
684
|
+
}
|
|
685
|
+
function serializeLegacyContent(parts) {
|
|
686
|
+
const text = parts.filter(isTextLikePart$1).map((part) => part.text).join("");
|
|
687
|
+
if (text.length > 0) return text;
|
|
688
|
+
if (parts.length === 0) return "";
|
|
689
|
+
return structuredClone(parts);
|
|
690
|
+
}
|
|
691
|
+
function extractTextFromNcpMessage(message) {
|
|
692
|
+
if (!message) return "";
|
|
693
|
+
return (message.role === "assistant" ? sanitizeAssistantReplyTags(message) : message).parts.filter(isTextLikePart$1).map((part) => part.text).join("");
|
|
694
|
+
}
|
|
695
|
+
function isTextLikePart$1(part) {
|
|
696
|
+
return part.type === "text" || part.type === "rich-text";
|
|
697
|
+
}
|
|
698
|
+
function buildLegacyUserContent(parts, options = {}) {
|
|
699
|
+
const content = buildNcpUserContent(parts, { assetStore: options.assetStore });
|
|
700
|
+
if (content === "") return serializeLegacyContent(parts);
|
|
701
|
+
return content;
|
|
702
|
+
}
|
|
703
|
+
function buildLegacyAssistantMessages(message, timestamp) {
|
|
704
|
+
const textContent = extractTextFromNcpMessage(message);
|
|
705
|
+
const reasoningParts = message.parts.filter((part) => part.type === "reasoning");
|
|
706
|
+
const reasoningContent = reasoningParts.map((part) => part.text).join("");
|
|
707
|
+
const toolInvocations = message.parts.filter((part) => part.type === "tool-invocation");
|
|
708
|
+
const assistantMessage = {
|
|
709
|
+
role: "assistant",
|
|
710
|
+
content: textContent,
|
|
711
|
+
timestamp,
|
|
712
|
+
ncp_message_id: message.id,
|
|
713
|
+
ncp_parts: structuredClone(message.parts)
|
|
714
|
+
};
|
|
715
|
+
if (typeof message.metadata?.reply_to === "string" && message.metadata.reply_to.trim().length > 0) assistantMessage.reply_to = message.metadata.reply_to.trim();
|
|
716
|
+
if (reasoningParts.length > 0) assistantMessage.reasoning_content = reasoningContent;
|
|
717
|
+
if (toolInvocations.length > 0) assistantMessage.tool_calls = toolInvocations.map((toolInvocation, index) => ({
|
|
718
|
+
id: toolInvocation.toolCallId ?? `${message.id}:tool:${index}`,
|
|
719
|
+
type: "function",
|
|
720
|
+
function: {
|
|
721
|
+
name: toolInvocation.toolName,
|
|
722
|
+
arguments: serializeToolArgs(toolInvocation.args)
|
|
723
|
+
}
|
|
724
|
+
}));
|
|
725
|
+
const messages = [assistantMessage];
|
|
726
|
+
for (const toolInvocation of toolInvocations) {
|
|
727
|
+
if (toolInvocation.state !== "result") continue;
|
|
728
|
+
messages.push({
|
|
729
|
+
role: "tool",
|
|
730
|
+
name: toolInvocation.toolName,
|
|
731
|
+
tool_call_id: toolInvocation.toolCallId,
|
|
732
|
+
content: typeof toolInvocation.result === "string" ? toolInvocation.result : JSON.stringify(toolInvocation.result ?? null),
|
|
733
|
+
...toolInvocation.resultContentItems ? { ncp_tool_result_content_items: structuredClone(toolInvocation.resultContentItems) } : {},
|
|
734
|
+
timestamp,
|
|
735
|
+
ncp_message_id: message.id
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return messages;
|
|
739
|
+
}
|
|
740
|
+
function buildLegacyNonAssistantMessage(message, timestamp, options) {
|
|
741
|
+
return {
|
|
742
|
+
role: message.role === "service" && options.serviceRole === "system" ? "system" : message.role,
|
|
743
|
+
content: buildLegacyUserContent(message.parts, options),
|
|
744
|
+
timestamp,
|
|
745
|
+
ncp_message_id: message.id,
|
|
746
|
+
ncp_parts: structuredClone(message.parts),
|
|
747
|
+
...message.metadata ? { ncp_metadata: structuredClone(message.metadata) } : {}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
function toLegacyMessages(messages, options = {}) {
|
|
751
|
+
const legacyMessages = [];
|
|
752
|
+
for (const rawMessage of messages) {
|
|
753
|
+
const message = rawMessage.role === "assistant" ? sanitizeAssistantReplyTags(rawMessage) : rawMessage;
|
|
754
|
+
const timestamp = ensureIsoTimestamp(message.timestamp, (/* @__PURE__ */ new Date()).toISOString());
|
|
755
|
+
if (message.role === "assistant") {
|
|
756
|
+
legacyMessages.push(...buildLegacyAssistantMessages(message, timestamp));
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
legacyMessages.push(buildLegacyNonAssistantMessage(message, timestamp, options));
|
|
760
|
+
}
|
|
761
|
+
return legacyMessages;
|
|
762
|
+
}
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/features/ncp-dispatch/utils/nextclaw-ncp-runner.utils.ts
|
|
765
|
+
function normalizeOptionalString$2(value) {
|
|
766
|
+
if (typeof value !== "string") return;
|
|
767
|
+
return value.trim() || void 0;
|
|
768
|
+
}
|
|
769
|
+
function resolveAttachmentName(attachment) {
|
|
770
|
+
const explicitName = normalizeOptionalString$2(attachment.name);
|
|
771
|
+
if (explicitName) return explicitName;
|
|
772
|
+
const explicitPath = normalizeOptionalString$2(attachment.path);
|
|
773
|
+
if (explicitPath) return basename(explicitPath);
|
|
774
|
+
const explicitUrl = normalizeOptionalString$2(attachment.url);
|
|
775
|
+
if (explicitUrl) try {
|
|
776
|
+
return basename(new URL(explicitUrl).pathname) || "asset.bin";
|
|
777
|
+
} catch {
|
|
778
|
+
return basename(explicitUrl) || "asset.bin";
|
|
779
|
+
}
|
|
780
|
+
return "asset.bin";
|
|
781
|
+
}
|
|
782
|
+
async function attachmentToPart(attachment, assetApi) {
|
|
783
|
+
const assetUri = normalizeOptionalString$2(attachment.assetUri);
|
|
784
|
+
if (assetUri) return createFilePartFromAssetUri(attachment, assetUri);
|
|
785
|
+
const remoteUrl = normalizeOptionalString$2(attachment.url);
|
|
786
|
+
const localPath = normalizeOptionalString$2(attachment.path);
|
|
787
|
+
if (localPath) return await createFilePartFromLocalPath(attachment, localPath, assetApi);
|
|
788
|
+
if (remoteUrl) return createFilePartFromRemoteUrl(attachment, remoteUrl);
|
|
789
|
+
throw new Error(`Unsupported attachment payload for "${resolveAttachmentName(attachment)}".`);
|
|
790
|
+
}
|
|
791
|
+
function createBaseFilePart(attachment) {
|
|
792
|
+
return {
|
|
793
|
+
...attachment.name ? { name: attachment.name } : {},
|
|
794
|
+
...attachment.mimeType ? { mimeType: attachment.mimeType } : {},
|
|
795
|
+
...typeof attachment.size === "number" ? { sizeBytes: attachment.size } : {}
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function createFilePartFromAssetUri(attachment, assetUri) {
|
|
799
|
+
return {
|
|
800
|
+
type: "file",
|
|
801
|
+
assetUri,
|
|
802
|
+
...createBaseFilePart(attachment)
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
async function createFilePartFromLocalPath(attachment, localPath, assetApi) {
|
|
806
|
+
if (!assetApi) throw new Error("NCP asset api is unavailable for local attachments.");
|
|
807
|
+
const fileName = resolveAttachmentName(attachment);
|
|
808
|
+
const bytes = await readFile(localPath);
|
|
809
|
+
return {
|
|
810
|
+
type: "file",
|
|
811
|
+
assetUri: (await assetApi.put({
|
|
812
|
+
fileName,
|
|
813
|
+
mimeType: attachment.mimeType ?? null,
|
|
814
|
+
bytes
|
|
815
|
+
})).uri,
|
|
816
|
+
name: attachment.name ?? fileName,
|
|
817
|
+
...attachment.mimeType ? { mimeType: attachment.mimeType } : {},
|
|
818
|
+
...typeof attachment.size === "number" ? { sizeBytes: attachment.size } : {}
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function createFilePartFromRemoteUrl(attachment, remoteUrl) {
|
|
822
|
+
return {
|
|
823
|
+
type: "file",
|
|
824
|
+
url: remoteUrl,
|
|
825
|
+
name: attachment.name ?? resolveAttachmentName(attachment),
|
|
826
|
+
...attachment.mimeType ? { mimeType: attachment.mimeType } : {},
|
|
827
|
+
...typeof attachment.size === "number" ? { sizeBytes: attachment.size } : {}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
async function buildUserMessageParts(params) {
|
|
831
|
+
const { assetApi, attachments, content } = params;
|
|
832
|
+
const parts = [];
|
|
833
|
+
if (content.length > 0) parts.push({
|
|
834
|
+
type: "text",
|
|
835
|
+
text: content
|
|
836
|
+
});
|
|
837
|
+
for (const attachment of attachments ?? []) parts.push(await attachmentToPart(attachment, assetApi));
|
|
838
|
+
return parts;
|
|
839
|
+
}
|
|
840
|
+
async function buildNcpUserMessage(params) {
|
|
841
|
+
const { assetApi, attachments, content, metadata, sessionId } = params;
|
|
842
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
843
|
+
return {
|
|
844
|
+
id: `${sessionId}:user:${timestamp}`,
|
|
845
|
+
sessionId,
|
|
846
|
+
role: "user",
|
|
847
|
+
status: "final",
|
|
848
|
+
timestamp,
|
|
849
|
+
parts: await buildUserMessageParts({
|
|
850
|
+
content,
|
|
851
|
+
attachments,
|
|
852
|
+
assetApi
|
|
853
|
+
}),
|
|
854
|
+
metadata: structuredClone(metadata ?? {})
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
async function* streamPromptOverNcp(params) {
|
|
858
|
+
const { abortSignal, agent, attachments, content, metadata, onEvent, sessionId } = params;
|
|
859
|
+
const message = await buildNcpUserMessage({
|
|
860
|
+
sessionId,
|
|
861
|
+
content,
|
|
862
|
+
attachments,
|
|
863
|
+
metadata,
|
|
864
|
+
assetApi: agent.assetApi
|
|
865
|
+
});
|
|
866
|
+
for await (const event of agent.runApi.send({
|
|
867
|
+
sessionId,
|
|
868
|
+
message,
|
|
869
|
+
metadata
|
|
870
|
+
}, { ...abortSignal ? { signal: abortSignal } : {} })) {
|
|
871
|
+
onEvent?.(event);
|
|
872
|
+
yield event;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async function runPromptOverNcp(params) {
|
|
876
|
+
let completedMessage;
|
|
877
|
+
for await (const event of streamPromptOverNcp(params)) {
|
|
878
|
+
if (event.type === NcpEventType.MessageTextDelta) {
|
|
879
|
+
params.onAssistantDelta?.(event.payload.delta);
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (event.type === NcpEventType.MessageFailed) throw new Error(event.payload.error.message);
|
|
883
|
+
if (event.type === NcpEventType.RunError) throw new Error(event.payload.error ?? params.runErrorMessage ?? "NCP run failed.");
|
|
884
|
+
if (event.type === NcpEventType.MessageCompleted) completedMessage = event.payload.message;
|
|
885
|
+
}
|
|
886
|
+
if (!completedMessage) throw new Error(params.missingCompletedMessageError ?? "NCP run completed without a final assistant message.");
|
|
887
|
+
return {
|
|
888
|
+
text: extractTextFromNcpMessage(completedMessage),
|
|
889
|
+
completedMessage
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
//#endregion
|
|
893
|
+
//#region src/features/ncp-dispatch/utils/channel-reply.utils.ts
|
|
894
|
+
function readString(value) {
|
|
895
|
+
if (typeof value !== "string") return;
|
|
896
|
+
return value.trim() || void 0;
|
|
897
|
+
}
|
|
898
|
+
function isReplyCapableChannel(channel) {
|
|
899
|
+
return Boolean(channel) && typeof channel.consumeNcpReply === "function";
|
|
900
|
+
}
|
|
901
|
+
function resolveChannelReplyRoute(params) {
|
|
902
|
+
const { channel, message, route } = params;
|
|
903
|
+
if (!isReplyCapableChannel(channel)) return null;
|
|
904
|
+
const metadata = structuredClone(message.metadata ?? {});
|
|
905
|
+
const accountId = readString(route.accountId) ?? readString(metadata.accountId) ?? readString(metadata.account_id);
|
|
906
|
+
return {
|
|
907
|
+
target: {
|
|
908
|
+
conversationId: message.chatId,
|
|
909
|
+
...accountId ? { accountId } : {},
|
|
910
|
+
...Object.keys(metadata).length > 0 ? { metadata } : {}
|
|
911
|
+
},
|
|
912
|
+
channel
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
async function dispatchChannelReplyRoute(params) {
|
|
916
|
+
const { abortSignal, agent, attachments, content, metadata, onEvent, route, sessionId } = params;
|
|
917
|
+
const input = {
|
|
918
|
+
target: {
|
|
919
|
+
...route.target,
|
|
920
|
+
...agent.assetApi?.resolveContentPath ? { resolveAssetContentPath: agent.assetApi.resolveContentPath } : {}
|
|
921
|
+
},
|
|
922
|
+
eventStream: streamPromptOverNcp({
|
|
923
|
+
agent,
|
|
924
|
+
sessionId,
|
|
925
|
+
content,
|
|
926
|
+
attachments,
|
|
927
|
+
metadata,
|
|
928
|
+
abortSignal,
|
|
929
|
+
onEvent
|
|
930
|
+
})
|
|
931
|
+
};
|
|
932
|
+
await route.channel.consumeNcpReply(input);
|
|
933
|
+
}
|
|
934
|
+
//#endregion
|
|
935
|
+
//#region src/features/ncp-dispatch/utils/nextclaw-ncp-dispatch.utils.ts
|
|
936
|
+
function normalizeOptionalString$1(value) {
|
|
937
|
+
if (typeof value !== "string") return;
|
|
938
|
+
return value.trim() || void 0;
|
|
939
|
+
}
|
|
940
|
+
function requireNcpAgent(resolveNcpAgent, purpose) {
|
|
941
|
+
const agent = resolveNcpAgent?.() ?? null;
|
|
942
|
+
if (!agent) throw new Error(`NCP agent is not ready for ${purpose}.`);
|
|
943
|
+
return agent;
|
|
944
|
+
}
|
|
945
|
+
function createDirectInboundMessage(params) {
|
|
946
|
+
const { attachments, channel, chatId, content, metadata } = params;
|
|
947
|
+
return {
|
|
948
|
+
channel: channel ?? "cli",
|
|
949
|
+
senderId: "user",
|
|
950
|
+
chatId: chatId ?? "direct",
|
|
951
|
+
content,
|
|
952
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
953
|
+
attachments: attachments ?? [],
|
|
954
|
+
metadata: structuredClone(metadata ?? {})
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function buildRunMetadata(params) {
|
|
958
|
+
const { message, metadata, route } = params;
|
|
959
|
+
return {
|
|
960
|
+
...message.metadata ?? {},
|
|
961
|
+
...metadata ?? {},
|
|
962
|
+
channel: message.channel,
|
|
963
|
+
chatId: message.chatId,
|
|
964
|
+
chat_id: message.chatId,
|
|
965
|
+
accountId: route.accountId,
|
|
966
|
+
account_id: route.accountId,
|
|
967
|
+
agentId: route.agentId,
|
|
968
|
+
agent_id: route.agentId,
|
|
969
|
+
sessionKey: route.sessionKey,
|
|
970
|
+
session_key: route.sessionKey,
|
|
971
|
+
senderId: message.senderId,
|
|
972
|
+
sender_id: message.senderId
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
async function executeSlashCommandMaybe(params) {
|
|
976
|
+
const { channel, chatId, config, rawContent, sessionKey, sessionManager } = params;
|
|
977
|
+
if (!rawContent.trim().startsWith("/")) return null;
|
|
978
|
+
return (await new CommandRegistry(config, sessionManager).executeText(rawContent, {
|
|
979
|
+
channel,
|
|
980
|
+
chatId,
|
|
981
|
+
senderId: "user",
|
|
982
|
+
sessionKey
|
|
983
|
+
}))?.content ?? null;
|
|
984
|
+
}
|
|
985
|
+
function resolveDirectRoute(params) {
|
|
986
|
+
const { agentId, config, sessionKey } = params;
|
|
987
|
+
const message = createDirectInboundMessage(params);
|
|
988
|
+
const forcedAgentId = normalizeOptionalString$1(agentId) ?? parseAgentScopedSessionKey(sessionKey)?.agentId ?? void 0;
|
|
989
|
+
return {
|
|
990
|
+
message,
|
|
991
|
+
route: new AgentRouteResolver(config).resolveInbound({
|
|
992
|
+
message,
|
|
993
|
+
forcedAgentId,
|
|
994
|
+
sessionKeyOverride: sessionKey
|
|
995
|
+
})
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function formatUserFacingError(error, maxChars = 320) {
|
|
999
|
+
const normalized = (error instanceof Error ? error.message || error.name || "Unknown error" : String(error ?? "Unknown error")).replace(/\s+/g, " ").trim();
|
|
1000
|
+
if (!normalized) return "Unknown error";
|
|
1001
|
+
if (normalized.length <= maxChars) return normalized;
|
|
1002
|
+
return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
1003
|
+
}
|
|
1004
|
+
async function dispatchChannelReplyRouteMaybe(runtime, context) {
|
|
1005
|
+
const { message, route, agent, runMetadata } = context;
|
|
1006
|
+
if (message.channel === "system") return false;
|
|
1007
|
+
const replyRoute = resolveChannelReplyRoute({
|
|
1008
|
+
channel: runtime.kernel.channels.getChannel(message.channel),
|
|
1009
|
+
message,
|
|
1010
|
+
route
|
|
1011
|
+
});
|
|
1012
|
+
if (!replyRoute) return false;
|
|
1013
|
+
await dispatchChannelReplyRoute({
|
|
1014
|
+
agent,
|
|
1015
|
+
route: replyRoute,
|
|
1016
|
+
sessionId: route.sessionKey,
|
|
1017
|
+
content: message.content,
|
|
1018
|
+
attachments: message.attachments,
|
|
1019
|
+
metadata: runMetadata
|
|
1020
|
+
});
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
async function publishLegacyReply(runtime, context, result) {
|
|
1024
|
+
const { message, route } = context;
|
|
1025
|
+
if (message.channel === "system") {
|
|
1026
|
+
runtime.appEventBus?.emit(eventKeys.sessionUpdated, { sessionKey: route.sessionKey }, {
|
|
1027
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1028
|
+
source: "backend"
|
|
1029
|
+
});
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (!result.text.trim()) {
|
|
1033
|
+
await runtime.messageBus.publishOutbound(createTypingStopControlMessage(message));
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
await runtime.messageBus.publishOutbound({
|
|
1037
|
+
channel: message.channel,
|
|
1038
|
+
chatId: message.chatId,
|
|
1039
|
+
content: result.text,
|
|
1040
|
+
media: [],
|
|
1041
|
+
metadata: buildRunMetadata({
|
|
1042
|
+
message,
|
|
1043
|
+
route,
|
|
1044
|
+
metadata: result.completedMessage.metadata
|
|
1045
|
+
})
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
async function dispatchPromptOverNcp(params) {
|
|
1049
|
+
const { abortSignal, agentId, attachments, channel, chatId, config, content, metadata, onAssistantDelta, resolveNcpAgent, sessionKey, sessionManager } = params;
|
|
1050
|
+
const { message, route } = resolveDirectRoute({
|
|
1051
|
+
config,
|
|
1052
|
+
content,
|
|
1053
|
+
sessionKey,
|
|
1054
|
+
channel,
|
|
1055
|
+
chatId,
|
|
1056
|
+
attachments,
|
|
1057
|
+
metadata,
|
|
1058
|
+
agentId
|
|
1059
|
+
});
|
|
1060
|
+
const commandResult = await executeSlashCommandMaybe({
|
|
1061
|
+
config,
|
|
1062
|
+
sessionManager,
|
|
1063
|
+
rawContent: content,
|
|
1064
|
+
channel: message.channel,
|
|
1065
|
+
chatId: message.chatId,
|
|
1066
|
+
sessionKey: route.sessionKey
|
|
1067
|
+
});
|
|
1068
|
+
if (commandResult) return commandResult;
|
|
1069
|
+
return (await runPromptOverNcp({
|
|
1070
|
+
agent: requireNcpAgent(resolveNcpAgent, "direct dispatch"),
|
|
1071
|
+
sessionId: route.sessionKey,
|
|
1072
|
+
content,
|
|
1073
|
+
attachments,
|
|
1074
|
+
metadata: buildRunMetadata({
|
|
1075
|
+
message,
|
|
1076
|
+
route
|
|
1077
|
+
}),
|
|
1078
|
+
abortSignal,
|
|
1079
|
+
onAssistantDelta,
|
|
1080
|
+
missingCompletedMessageError: `session "${route.sessionKey}" completed without a final assistant message`,
|
|
1081
|
+
runErrorMessage: `session "${route.sessionKey}" failed`
|
|
1082
|
+
})).text;
|
|
1083
|
+
}
|
|
1084
|
+
async function runGatewayInboundLoop(runtime) {
|
|
1085
|
+
while (true) {
|
|
1086
|
+
const message = await runtime.messageBus.consumeInbound();
|
|
1087
|
+
let usedChannelReplyRoute = false;
|
|
1088
|
+
try {
|
|
1089
|
+
const explicitSessionKey = normalizeOptionalString$1(message.metadata.session_key_override);
|
|
1090
|
+
const forcedAgentId = normalizeOptionalString$1(message.metadata.target_agent_id);
|
|
1091
|
+
const route = new AgentRouteResolver(runtime.configManager.loadConfig()).resolveInbound({
|
|
1092
|
+
message,
|
|
1093
|
+
forcedAgentId,
|
|
1094
|
+
sessionKeyOverride: explicitSessionKey
|
|
1095
|
+
});
|
|
1096
|
+
const agent = requireNcpAgent(() => runtime.liveAgentRuntime, "gateway dispatch");
|
|
1097
|
+
const runMetadata = buildRunMetadata({
|
|
1098
|
+
message,
|
|
1099
|
+
route
|
|
1100
|
+
});
|
|
1101
|
+
const context = {
|
|
1102
|
+
agent,
|
|
1103
|
+
message,
|
|
1104
|
+
route,
|
|
1105
|
+
runMetadata
|
|
1106
|
+
};
|
|
1107
|
+
if (await dispatchChannelReplyRouteMaybe(runtime, context)) {
|
|
1108
|
+
usedChannelReplyRoute = true;
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
if (message.channel !== "system") await runtime.messageBus.publishOutbound(createAssistantStreamResetControlMessage(message));
|
|
1112
|
+
await publishLegacyReply(runtime, context, await runPromptOverNcp({
|
|
1113
|
+
agent,
|
|
1114
|
+
sessionId: route.sessionKey,
|
|
1115
|
+
content: message.content,
|
|
1116
|
+
attachments: message.attachments,
|
|
1117
|
+
metadata: runMetadata,
|
|
1118
|
+
onAssistantDelta: message.channel !== "system" ? (delta) => {
|
|
1119
|
+
if (!delta) return;
|
|
1120
|
+
runtime.messageBus.publishOutbound(createAssistantStreamDeltaControlMessage(message, delta));
|
|
1121
|
+
} : void 0,
|
|
1122
|
+
missingCompletedMessageError: `session "${route.sessionKey}" completed without a final assistant message`,
|
|
1123
|
+
runErrorMessage: `session "${route.sessionKey}" failed`
|
|
1124
|
+
}));
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
if (usedChannelReplyRoute) continue;
|
|
1127
|
+
await runtime.messageBus.publishOutbound({
|
|
1128
|
+
channel: message.channel,
|
|
1129
|
+
chatId: message.chatId,
|
|
1130
|
+
content: `Sorry, I encountered an error: ${formatUserFacingError(error)}`,
|
|
1131
|
+
media: [],
|
|
1132
|
+
metadata: {}
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
//#endregion
|
|
1138
|
+
//#region src/utils/ncp-session-message-adapter.utils.ts
|
|
1139
|
+
function tryParseJson(value) {
|
|
1140
|
+
try {
|
|
1141
|
+
return JSON.parse(value);
|
|
1142
|
+
} catch {
|
|
1143
|
+
return value;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
function toTextPart(text) {
|
|
1147
|
+
return text.length > 0 ? {
|
|
1148
|
+
type: "text",
|
|
1149
|
+
text
|
|
1150
|
+
} : null;
|
|
1151
|
+
}
|
|
1152
|
+
function contentToParts(content) {
|
|
1153
|
+
if (typeof content === "string") {
|
|
1154
|
+
const textPart = toTextPart(content);
|
|
1155
|
+
return textPart ? [textPart] : [];
|
|
1156
|
+
}
|
|
1157
|
+
if (Array.isArray(content)) return content.length > 0 ? [{
|
|
1158
|
+
type: "extension",
|
|
1159
|
+
extensionType: "nextclaw.legacy.content-array",
|
|
1160
|
+
data: structuredClone(content)
|
|
1161
|
+
}] : [];
|
|
1162
|
+
if (content && typeof content === "object") return [{
|
|
1163
|
+
type: "extension",
|
|
1164
|
+
extensionType: "nextclaw.legacy.content-object",
|
|
1165
|
+
data: structuredClone(content)
|
|
1166
|
+
}];
|
|
1167
|
+
return [];
|
|
1168
|
+
}
|
|
1169
|
+
function normalizeToolResultPayload(value) {
|
|
1170
|
+
if (typeof value !== "string") return structuredClone(value);
|
|
1171
|
+
const trimmed = value.trim();
|
|
1172
|
+
if (!trimmed) return value;
|
|
1173
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
|
|
1174
|
+
return tryParseJson(trimmed);
|
|
1175
|
+
}
|
|
1176
|
+
function parseLegacyToolCalls(value) {
|
|
1177
|
+
if (!Array.isArray(value)) return [];
|
|
1178
|
+
return value.map((entry) => {
|
|
1179
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
1180
|
+
const toolCall = entry;
|
|
1181
|
+
const id = normalizeString(toolCall.id);
|
|
1182
|
+
const rawFunction = toolCall.function;
|
|
1183
|
+
if (!id || !rawFunction || typeof rawFunction !== "object" || Array.isArray(rawFunction)) return null;
|
|
1184
|
+
const fn = rawFunction;
|
|
1185
|
+
const name = normalizeString(fn.name);
|
|
1186
|
+
const args = typeof fn.arguments === "string" ? fn.arguments : JSON.stringify(fn.arguments ?? {});
|
|
1187
|
+
if (!name) return null;
|
|
1188
|
+
return {
|
|
1189
|
+
id,
|
|
1190
|
+
type: "function",
|
|
1191
|
+
function: {
|
|
1192
|
+
name,
|
|
1193
|
+
arguments: args
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
}).filter((entry) => entry !== null);
|
|
1197
|
+
}
|
|
1198
|
+
function createMessageId(sessionId, index, role, timestamp) {
|
|
1199
|
+
return `${sessionId}:${role.trim().toLowerCase() || "message"}:${index}:${timestamp}`;
|
|
1200
|
+
}
|
|
1201
|
+
function resolveMessageId(params) {
|
|
1202
|
+
const { index, message, role, sessionId, timestamp } = params;
|
|
1203
|
+
return normalizeString(message.ncp_message_id) ?? createMessageId(sessionId, index, role, timestamp);
|
|
1204
|
+
}
|
|
1205
|
+
function isNcpMessagePart(value) {
|
|
1206
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && typeof value.type === "string";
|
|
1207
|
+
}
|
|
1208
|
+
function readStoredNcpParts(message) {
|
|
1209
|
+
const rawParts = message.ncp_parts;
|
|
1210
|
+
if (!Array.isArray(rawParts)) return null;
|
|
1211
|
+
const parts = rawParts.filter((part) => isNcpMessagePart(part));
|
|
1212
|
+
if (parts.length !== rawParts.length) return null;
|
|
1213
|
+
return structuredClone(parts);
|
|
1214
|
+
}
|
|
1215
|
+
function readStoredNcpMetadata(message) {
|
|
1216
|
+
const rawMetadata = message.ncp_metadata;
|
|
1217
|
+
if (!rawMetadata || typeof rawMetadata !== "object" || Array.isArray(rawMetadata)) return;
|
|
1218
|
+
return structuredClone(rawMetadata);
|
|
1219
|
+
}
|
|
1220
|
+
function buildAssistantMessage(params) {
|
|
1221
|
+
const { index, message, sessionId } = params;
|
|
1222
|
+
const timestamp = ensureIsoTimestamp(message.timestamp, (/* @__PURE__ */ new Date()).toISOString());
|
|
1223
|
+
const replyTo = typeof message.reply_to === "string" && message.reply_to.trim().length > 0 ? message.reply_to.trim() : void 0;
|
|
1224
|
+
const storedParts = readStoredNcpParts(message);
|
|
1225
|
+
if (storedParts) return sanitizeAssistantReplyTags({
|
|
1226
|
+
id: resolveMessageId({
|
|
1227
|
+
index,
|
|
1228
|
+
message,
|
|
1229
|
+
role: "assistant",
|
|
1230
|
+
sessionId,
|
|
1231
|
+
timestamp
|
|
1232
|
+
}),
|
|
1233
|
+
sessionId,
|
|
1234
|
+
role: "assistant",
|
|
1235
|
+
status: "final",
|
|
1236
|
+
timestamp,
|
|
1237
|
+
parts: storedParts,
|
|
1238
|
+
metadata: replyTo ? { reply_to: replyTo } : void 0
|
|
1239
|
+
});
|
|
1240
|
+
const toolCalls = parseLegacyToolCalls(message.tool_calls);
|
|
1241
|
+
const parts = [...contentToParts(message.content)];
|
|
1242
|
+
if (typeof message.reasoning_content === "string") parts.push({
|
|
1243
|
+
type: "reasoning",
|
|
1244
|
+
text: message.reasoning_content
|
|
1245
|
+
});
|
|
1246
|
+
for (const toolCall of toolCalls) parts.push({
|
|
1247
|
+
type: "tool-invocation",
|
|
1248
|
+
toolCallId: toolCall.id,
|
|
1249
|
+
toolName: toolCall.function.name,
|
|
1250
|
+
state: "call",
|
|
1251
|
+
args: tryParseJson(toolCall.function.arguments)
|
|
1252
|
+
});
|
|
1253
|
+
return sanitizeAssistantReplyTags({
|
|
1254
|
+
id: resolveMessageId({
|
|
1255
|
+
index,
|
|
1256
|
+
message,
|
|
1257
|
+
role: "assistant",
|
|
1258
|
+
sessionId,
|
|
1259
|
+
timestamp
|
|
1260
|
+
}),
|
|
1261
|
+
sessionId,
|
|
1262
|
+
role: "assistant",
|
|
1263
|
+
status: "final",
|
|
1264
|
+
timestamp,
|
|
1265
|
+
parts,
|
|
1266
|
+
metadata: replyTo ? { reply_to: replyTo } : void 0
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
function buildGenericMessage(params) {
|
|
1270
|
+
const { index, message, role, sessionId } = params;
|
|
1271
|
+
const timestamp = ensureIsoTimestamp(message.timestamp, (/* @__PURE__ */ new Date()).toISOString());
|
|
1272
|
+
const storedParts = readStoredNcpParts(message);
|
|
1273
|
+
const storedMetadata = readStoredNcpMetadata(message);
|
|
1274
|
+
if (storedParts) return {
|
|
1275
|
+
id: resolveMessageId({
|
|
1276
|
+
index,
|
|
1277
|
+
message,
|
|
1278
|
+
role,
|
|
1279
|
+
sessionId,
|
|
1280
|
+
timestamp
|
|
1281
|
+
}),
|
|
1282
|
+
sessionId,
|
|
1283
|
+
role,
|
|
1284
|
+
status: "final",
|
|
1285
|
+
timestamp,
|
|
1286
|
+
parts: storedParts,
|
|
1287
|
+
...storedMetadata ? { metadata: storedMetadata } : {}
|
|
1288
|
+
};
|
|
1289
|
+
return {
|
|
1290
|
+
id: resolveMessageId({
|
|
1291
|
+
index,
|
|
1292
|
+
message,
|
|
1293
|
+
role,
|
|
1294
|
+
sessionId,
|
|
1295
|
+
timestamp
|
|
1296
|
+
}),
|
|
1297
|
+
sessionId,
|
|
1298
|
+
role,
|
|
1299
|
+
status: "final",
|
|
1300
|
+
timestamp,
|
|
1301
|
+
parts: contentToParts(message.content),
|
|
1302
|
+
...storedMetadata ? { metadata: storedMetadata } : {}
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
function attachToolResult(target, toolCallId, result, toolName, resultContentItems) {
|
|
1306
|
+
const normalizedResult = normalizeToolResultPayload(result);
|
|
1307
|
+
const normalizedContentItems = Array.isArray(resultContentItems) ? structuredClone(resultContentItems) : void 0;
|
|
1308
|
+
return {
|
|
1309
|
+
...target,
|
|
1310
|
+
parts: target.parts.map((part) => {
|
|
1311
|
+
if (part.type !== "tool-invocation" || part.toolCallId !== toolCallId) return part;
|
|
1312
|
+
if (part.state === "result" && Object.prototype.hasOwnProperty.call(part, "result")) return {
|
|
1313
|
+
...part,
|
|
1314
|
+
...toolName ? { toolName } : {}
|
|
1315
|
+
};
|
|
1316
|
+
return {
|
|
1317
|
+
...part,
|
|
1318
|
+
toolName: toolName ?? part.toolName,
|
|
1319
|
+
state: "result",
|
|
1320
|
+
result: normalizedResult,
|
|
1321
|
+
...normalizedContentItems ? { resultContentItems: normalizedContentItems } : {}
|
|
1322
|
+
};
|
|
1323
|
+
})
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
function toNcpMessages(sessionId, messages) {
|
|
1327
|
+
const ncpMessages = [];
|
|
1328
|
+
const assistantIndexByToolCallId = /* @__PURE__ */ new Map();
|
|
1329
|
+
messages.forEach((message, index) => {
|
|
1330
|
+
const role = normalizeString(message.role)?.toLowerCase() ?? "assistant";
|
|
1331
|
+
if (role === "tool") {
|
|
1332
|
+
const toolCallId = normalizeString(message.tool_call_id);
|
|
1333
|
+
if (toolCallId) {
|
|
1334
|
+
const assistantIndex = assistantIndexByToolCallId.get(toolCallId);
|
|
1335
|
+
if (assistantIndex !== void 0) {
|
|
1336
|
+
ncpMessages[assistantIndex] = attachToolResult(ncpMessages[assistantIndex], toolCallId, structuredClone(message.content), normalizeString(message.name) ?? void 0, message.ncp_tool_result_content_items);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
ncpMessages.push(buildGenericMessage({
|
|
1341
|
+
sessionId,
|
|
1342
|
+
index,
|
|
1343
|
+
role: "tool",
|
|
1344
|
+
message
|
|
1345
|
+
}));
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (role === "assistant") {
|
|
1349
|
+
const assistant = buildAssistantMessage({
|
|
1350
|
+
sessionId,
|
|
1351
|
+
index,
|
|
1352
|
+
message
|
|
1353
|
+
});
|
|
1354
|
+
const assistantPosition = ncpMessages.push(assistant) - 1;
|
|
1355
|
+
for (const part of assistant.parts) if (part.type === "tool-invocation" && part.toolCallId) assistantIndexByToolCallId.set(part.toolCallId, assistantPosition);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
const normalizedRole = role === "system" || role === "user" || role === "service" ? role : "user";
|
|
1359
|
+
ncpMessages.push(buildGenericMessage({
|
|
1360
|
+
sessionId,
|
|
1361
|
+
index,
|
|
1362
|
+
role: normalizedRole,
|
|
1363
|
+
message
|
|
1364
|
+
}));
|
|
1365
|
+
});
|
|
1366
|
+
return ncpMessages;
|
|
1367
|
+
}
|
|
1368
|
+
function resolveLegacyEventType(message) {
|
|
1369
|
+
const role = normalizeString(message.role)?.toLowerCase() ?? "";
|
|
1370
|
+
if (role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) return "assistant.tool_call";
|
|
1371
|
+
if (role === "tool") return "tool.result";
|
|
1372
|
+
if (role === "assistant") return "message.assistant";
|
|
1373
|
+
if (role === "user") return "message.user";
|
|
1374
|
+
if (role === "system") return "message.system";
|
|
1375
|
+
return `message.${role || "other"}`;
|
|
1376
|
+
}
|
|
1377
|
+
//#endregion
|
|
1378
|
+
//#region src/features/native-runtime/utils/context-compaction-timeline-message.utils.ts
|
|
1379
|
+
const NEXTCLAW_TIMELINE_KIND_METADATA_KEY = "nextclaw_timeline_kind";
|
|
1380
|
+
const CONTEXT_COMPACTION_TIMELINE_KIND = "context_compaction";
|
|
1381
|
+
function readTimelineMetadata(message) {
|
|
1382
|
+
const rawMetadata = message?.ncp_metadata;
|
|
1383
|
+
if (!rawMetadata || typeof rawMetadata !== "object" || Array.isArray(rawMetadata)) return null;
|
|
1384
|
+
const metadata = rawMetadata;
|
|
1385
|
+
if (metadata["nextclaw_timeline_kind"] !== "context_compaction") return null;
|
|
1386
|
+
const checkpoint = metadata.checkpoint && typeof metadata.checkpoint === "object" && !Array.isArray(metadata.checkpoint) ? metadata.checkpoint : null;
|
|
1387
|
+
if (!checkpoint) return null;
|
|
1388
|
+
return {
|
|
1389
|
+
[NEXTCLAW_TIMELINE_KIND_METADATA_KEY]: CONTEXT_COMPACTION_TIMELINE_KIND,
|
|
1390
|
+
checkpoint
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function buildTimelineMessage(checkpoint) {
|
|
1394
|
+
return {
|
|
1395
|
+
role: "service",
|
|
1396
|
+
content: checkpoint.status === "compressing" ? "正在压缩较早上下文" : "较早上下文已自动压缩",
|
|
1397
|
+
timestamp: checkpoint.updatedAt,
|
|
1398
|
+
ncp_parts: [{
|
|
1399
|
+
type: "text",
|
|
1400
|
+
text: checkpoint.status === "compressing" ? "正在压缩较早上下文" : "较早上下文已自动压缩"
|
|
1401
|
+
}],
|
|
1402
|
+
ncp_metadata: {
|
|
1403
|
+
[NEXTCLAW_TIMELINE_KIND_METADATA_KEY]: CONTEXT_COMPACTION_TIMELINE_KIND,
|
|
1404
|
+
checkpoint
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function buildContextCompactionTimelineNcpMessage(params) {
|
|
1409
|
+
const { checkpoint, sessionId } = params;
|
|
1410
|
+
const text = checkpoint.status === "compressing" ? "正在压缩较早上下文" : "较早上下文已自动压缩";
|
|
1411
|
+
return {
|
|
1412
|
+
id: `${sessionId}:service:context-compaction:${checkpoint.id}`,
|
|
1413
|
+
sessionId,
|
|
1414
|
+
role: "service",
|
|
1415
|
+
status: "final",
|
|
1416
|
+
timestamp: checkpoint.updatedAt,
|
|
1417
|
+
parts: [{
|
|
1418
|
+
type: "text",
|
|
1419
|
+
text
|
|
1420
|
+
}],
|
|
1421
|
+
metadata: {
|
|
1422
|
+
[NEXTCLAW_TIMELINE_KIND_METADATA_KEY]: CONTEXT_COMPACTION_TIMELINE_KIND,
|
|
1423
|
+
checkpoint
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
function upsertContextCompactionTimelineMessage(params) {
|
|
1428
|
+
const { checkpoint, insertBeforeMessageIds = [], session } = params;
|
|
1429
|
+
const nextMessage = buildTimelineMessage(checkpoint);
|
|
1430
|
+
for (let index = session.events.length - 1; index >= 0; index -= 1) {
|
|
1431
|
+
const event = session.events[index];
|
|
1432
|
+
const metadata = readTimelineMetadata(event?.data?.message && typeof event.data.message === "object" && !Array.isArray(event.data.message) ? event.data.message : null);
|
|
1433
|
+
if (!metadata) continue;
|
|
1434
|
+
event.timestamp = checkpoint.updatedAt;
|
|
1435
|
+
event.data.message = nextMessage;
|
|
1436
|
+
session.messages = session.messages.map((item) => {
|
|
1437
|
+
return readTimelineMetadata(item)?.checkpoint.id === metadata.checkpoint.id ? nextMessage : item;
|
|
1438
|
+
});
|
|
1439
|
+
session.updatedAt = new Date(checkpoint.updatedAt);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
const event = {
|
|
1443
|
+
seq: session.nextSeq,
|
|
1444
|
+
type: "message.service",
|
|
1445
|
+
timestamp: checkpoint.updatedAt,
|
|
1446
|
+
data: { message: nextMessage }
|
|
1447
|
+
};
|
|
1448
|
+
session.nextSeq += 1;
|
|
1449
|
+
const insertionIds = new Set(insertBeforeMessageIds);
|
|
1450
|
+
const messageIndex = session.messages.findIndex((message) => typeof message.ncp_message_id === "string" && insertionIds.has(message.ncp_message_id));
|
|
1451
|
+
if (messageIndex >= 0) session.messages.splice(messageIndex, 0, nextMessage);
|
|
1452
|
+
else session.messages.push(nextMessage);
|
|
1453
|
+
const eventIndex = session.events.findIndex((candidateEvent) => {
|
|
1454
|
+
const message = candidateEvent?.data?.message && typeof candidateEvent.data.message === "object" && !Array.isArray(candidateEvent.data.message) ? candidateEvent.data.message : null;
|
|
1455
|
+
return typeof message?.ncp_message_id === "string" && insertionIds.has(message.ncp_message_id);
|
|
1456
|
+
});
|
|
1457
|
+
if (eventIndex >= 0) session.events.splice(eventIndex, 0, event);
|
|
1458
|
+
else session.events.push(event);
|
|
1459
|
+
session.updatedAt = new Date(checkpoint.updatedAt);
|
|
1460
|
+
}
|
|
1461
|
+
function isContextCompactionTimelineMessage(message) {
|
|
1462
|
+
return message?.metadata?.[NEXTCLAW_TIMELINE_KIND_METADATA_KEY] === CONTEXT_COMPACTION_TIMELINE_KIND;
|
|
1463
|
+
}
|
|
1464
|
+
//#endregion
|
|
1465
|
+
//#region src/features/native-runtime/utils/context-compaction-projection.utils.ts
|
|
1466
|
+
function readCompactionSummary(message) {
|
|
1467
|
+
const metadata = message.metadata;
|
|
1468
|
+
if (metadata?.["nextclaw_timeline_kind"] !== "context_compaction") return null;
|
|
1469
|
+
return readCompressedContextCompactionCheckpoint(metadata.checkpoint)?.summary ?? null;
|
|
1470
|
+
}
|
|
1471
|
+
function projectNcpMessagesWithContextCompaction(params) {
|
|
1472
|
+
const { sessionId, sessionMessages } = params;
|
|
1473
|
+
let checkpointIndex = -1;
|
|
1474
|
+
let summary = "";
|
|
1475
|
+
for (let index = sessionMessages.length - 1; index >= 0; index -= 1) {
|
|
1476
|
+
const candidateSummary = readCompactionSummary(sessionMessages[index]);
|
|
1477
|
+
if (!candidateSummary) continue;
|
|
1478
|
+
checkpointIndex = index;
|
|
1479
|
+
summary = candidateSummary;
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
if (checkpointIndex < 0) return sessionMessages.filter((message) => !readCompactionSummary(message)).map((message) => structuredClone(message));
|
|
1483
|
+
return [{
|
|
1484
|
+
id: `${sessionId}:context-compaction-summary:${sessionMessages[checkpointIndex]?.id ?? "latest"}`,
|
|
1485
|
+
sessionId,
|
|
1486
|
+
role: "user",
|
|
1487
|
+
status: "final",
|
|
1488
|
+
timestamp: sessionMessages[checkpointIndex]?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1489
|
+
parts: [{
|
|
1490
|
+
type: "text",
|
|
1491
|
+
text: summary
|
|
1492
|
+
}]
|
|
1493
|
+
}, ...sessionMessages.slice(checkpointIndex + 1).filter((message) => !readCompactionSummary(message)).map((message) => structuredClone(message))];
|
|
1494
|
+
}
|
|
1495
|
+
//#endregion
|
|
1496
|
+
//#region src/features/native-runtime/services/context-compaction-preflight.service.ts
|
|
1497
|
+
const SUMMARY_MAX_TOKENS = 4e3;
|
|
1498
|
+
const SUMMARY_SOURCE_MAX_CHARS = 12e4;
|
|
1499
|
+
function readRequestedAgentId$1(metadata) {
|
|
1500
|
+
return normalizeString(metadata.agent_id)?.toLowerCase() ?? normalizeString(metadata.agentId)?.toLowerCase() ?? null;
|
|
1501
|
+
}
|
|
1502
|
+
function resolveCompactionProfile(params) {
|
|
1503
|
+
const { config, requestMetadata, storedAgentId } = params;
|
|
1504
|
+
const { defaults } = config.agents;
|
|
1505
|
+
const defaultAgentId = resolveDefaultAgentProfileId(config);
|
|
1506
|
+
const profile = findEffectiveAgentProfile(config, normalizeString(storedAgentId)?.toLowerCase() ?? readRequestedAgentId$1(requestMetadata) ?? defaultAgentId) ?? findEffectiveAgentProfile(config, defaultAgentId);
|
|
1507
|
+
return {
|
|
1508
|
+
contextTokens: profile?.contextTokens ?? defaults.contextTokens,
|
|
1509
|
+
model: profile?.model ?? defaults.model,
|
|
1510
|
+
reservedContextTokens: ContextWindowBudgetService.resolveReservedContextTokens({
|
|
1511
|
+
contextTokens: profile?.contextTokens ?? defaults.contextTokens,
|
|
1512
|
+
configuredReservedContextTokens: profile?.reservedContextTokens ?? defaults.reservedContextTokens
|
|
1513
|
+
})
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function mergeInputMessages(params) {
|
|
1517
|
+
const messages = params.sessionMessages.map((message) => structuredClone(message));
|
|
1518
|
+
const seen = new Set(messages.map((message) => message.id));
|
|
1519
|
+
for (const message of params.inputMessages) {
|
|
1520
|
+
if (seen.has(message.id)) continue;
|
|
1521
|
+
messages.push(structuredClone(message));
|
|
1522
|
+
}
|
|
1523
|
+
return messages;
|
|
1524
|
+
}
|
|
1525
|
+
function stringifyCompactionSource(messages) {
|
|
1526
|
+
const json = JSON.stringify(messages, null, 2);
|
|
1527
|
+
if (json.length <= SUMMARY_SOURCE_MAX_CHARS) return json;
|
|
1528
|
+
return `${json.slice(0, SUMMARY_SOURCE_MAX_CHARS).trimEnd()}\n[truncated_source]`;
|
|
1529
|
+
}
|
|
1530
|
+
function buildContextWindowSnapshotFromBudget(params) {
|
|
1531
|
+
const { budget, checkpoint, totalContextTokens } = params;
|
|
1532
|
+
return buildContextWindowSnapshot({
|
|
1533
|
+
usedContextTokens: budget.estimatedTokens,
|
|
1534
|
+
totalContextTokens,
|
|
1535
|
+
prunedUsedContextTokens: budget.estimatedTokens,
|
|
1536
|
+
droppedHistoryCount: budget.droppedHistoryCount,
|
|
1537
|
+
truncatedToolResultCount: budget.truncatedToolResultCount,
|
|
1538
|
+
truncatedSystemPrompt: budget.truncatedSystemPrompt,
|
|
1539
|
+
truncatedUserMessage: budget.truncatedUserMessage,
|
|
1540
|
+
checkpoint,
|
|
1541
|
+
compactedUsedContextTokens: checkpoint ? budget.estimatedTokens : void 0
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
var ContextCompactionPreflightService = class {
|
|
1545
|
+
compactionService = new ContextCompactionService();
|
|
1546
|
+
contextWindowBudgetService = new ContextWindowBudgetService();
|
|
1547
|
+
constructor(options) {
|
|
1548
|
+
this.options = options;
|
|
1549
|
+
}
|
|
1550
|
+
run = async (params) => {
|
|
1551
|
+
const beginResult = this.begin(params);
|
|
1552
|
+
if (!beginResult) return null;
|
|
1553
|
+
if (!beginResult.pendingCompaction) return beginResult;
|
|
1554
|
+
return await this.finish(beginResult.pendingCompaction);
|
|
1555
|
+
};
|
|
1556
|
+
preview = (params) => {
|
|
1557
|
+
const { contextWindowOwner, requestMetadata, sessionId, sessionMessages } = params;
|
|
1558
|
+
if (contextWindowOwner === "runtime") return null;
|
|
1559
|
+
const session = this.options.sessionManager.getIfExists(sessionId);
|
|
1560
|
+
const metadata = session?.metadata ?? requestMetadata;
|
|
1561
|
+
const profile = resolveCompactionProfile({
|
|
1562
|
+
config: this.options.getConfig(),
|
|
1563
|
+
requestMetadata,
|
|
1564
|
+
storedAgentId: session?.agentId
|
|
1565
|
+
});
|
|
1566
|
+
const existingCheckpoint = readCompressedContextCompactionCheckpoint(metadata[CONTEXT_COMPACTION_METADATA_KEY]);
|
|
1567
|
+
return this.buildContextWindowSnapshotForMessages({
|
|
1568
|
+
checkpoint: existingCheckpoint,
|
|
1569
|
+
contextTokens: profile.contextTokens,
|
|
1570
|
+
reservedContextTokens: profile.reservedContextTokens,
|
|
1571
|
+
sessionId,
|
|
1572
|
+
sessionMessages
|
|
1573
|
+
});
|
|
1574
|
+
};
|
|
1575
|
+
begin = (params) => {
|
|
1576
|
+
const { contextWindowOwner, inputMessages, requestMetadata, sessionId, sessionMessages } = params;
|
|
1577
|
+
if (contextWindowOwner === "runtime") return null;
|
|
1578
|
+
const session = this.options.sessionManager.getOrCreate(sessionId);
|
|
1579
|
+
const profile = resolveCompactionProfile({
|
|
1580
|
+
config: this.options.getConfig(),
|
|
1581
|
+
requestMetadata,
|
|
1582
|
+
storedAgentId: session.agentId
|
|
1583
|
+
});
|
|
1584
|
+
const { contextTokens, reservedContextTokens } = profile;
|
|
1585
|
+
const ncpMessages = mergeInputMessages({
|
|
1586
|
+
inputMessages,
|
|
1587
|
+
sessionMessages
|
|
1588
|
+
});
|
|
1589
|
+
const modelCandidateMessages = ncpMessages.filter((message) => !isContextCompactionTimelineMessage(message));
|
|
1590
|
+
const existingCheckpoint = readCompressedContextCompactionCheckpoint(session.metadata[CONTEXT_COMPACTION_METADATA_KEY]);
|
|
1591
|
+
const messages = toLegacyMessages(existingCheckpoint ? projectNcpMessagesWithContextCompaction({
|
|
1592
|
+
sessionId,
|
|
1593
|
+
sessionMessages: ncpMessages
|
|
1594
|
+
}) : modelCandidateMessages);
|
|
1595
|
+
const budget = this.contextWindowBudgetService.evaluate({
|
|
1596
|
+
messages,
|
|
1597
|
+
contextTokens,
|
|
1598
|
+
reservedContextTokens
|
|
1599
|
+
});
|
|
1600
|
+
const plan = existingCheckpoint || !budget.shouldCompact ? null : this.compactionService.prepareForModelInput({
|
|
1601
|
+
messages: budget.messages,
|
|
1602
|
+
contextTokens,
|
|
1603
|
+
compactionThresholdTokens: budget.triggerTokens
|
|
1604
|
+
});
|
|
1605
|
+
const checkpoint = plan ? {
|
|
1606
|
+
...buildCompressingCompactionCheckpoint(session.metadata[CONTEXT_COMPACTION_METADATA_KEY]),
|
|
1607
|
+
coveredMessageCount: plan.coveredMessages.length,
|
|
1608
|
+
coveredSessionMessageCount: plan.coveredMessages.length,
|
|
1609
|
+
originalEstimatedTokens: plan.originalEstimatedTokens,
|
|
1610
|
+
projectedEstimatedTokens: budget.estimatedTokens
|
|
1611
|
+
} : existingCheckpoint;
|
|
1612
|
+
const contextWindow = buildContextWindowSnapshotFromBudget({
|
|
1613
|
+
budget,
|
|
1614
|
+
checkpoint,
|
|
1615
|
+
totalContextTokens: contextTokens
|
|
1616
|
+
});
|
|
1617
|
+
if (plan && checkpoint) {
|
|
1618
|
+
session.metadata[CONTEXT_COMPACTION_METADATA_KEY] = checkpoint;
|
|
1619
|
+
upsertContextCompactionTimelineMessage({
|
|
1620
|
+
session,
|
|
1621
|
+
checkpoint,
|
|
1622
|
+
insertBeforeMessageIds: inputMessages.map((message) => message.id)
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
this.options.sessionManager.save(session);
|
|
1626
|
+
return {
|
|
1627
|
+
contextWindow,
|
|
1628
|
+
metadata: structuredClone(session.metadata),
|
|
1629
|
+
sessionMessages: toNcpMessages(sessionId, session.messages),
|
|
1630
|
+
timelineMessage: plan && checkpoint ? buildContextCompactionTimelineNcpMessage({
|
|
1631
|
+
sessionId,
|
|
1632
|
+
checkpoint
|
|
1633
|
+
}) : null,
|
|
1634
|
+
pendingCompaction: plan && checkpoint ? {
|
|
1635
|
+
checkpoint,
|
|
1636
|
+
contextTokens,
|
|
1637
|
+
inputMessageIds: inputMessages.map((message) => message.id),
|
|
1638
|
+
model: profile.model,
|
|
1639
|
+
plan,
|
|
1640
|
+
reservedContextTokens,
|
|
1641
|
+
sessionId
|
|
1642
|
+
} : null
|
|
1643
|
+
};
|
|
1644
|
+
};
|
|
1645
|
+
buildContextWindowSnapshotForMessages = (params) => {
|
|
1646
|
+
const { checkpoint, contextTokens, reservedContextTokens, sessionId, sessionMessages } = params;
|
|
1647
|
+
const modelCandidateMessages = sessionMessages.filter((message) => !isContextCompactionTimelineMessage(message));
|
|
1648
|
+
const messages = toLegacyMessages(checkpoint ? projectNcpMessagesWithContextCompaction({
|
|
1649
|
+
sessionId,
|
|
1650
|
+
sessionMessages
|
|
1651
|
+
}) : modelCandidateMessages);
|
|
1652
|
+
return buildContextWindowSnapshotFromBudget({
|
|
1653
|
+
budget: this.contextWindowBudgetService.evaluate({
|
|
1654
|
+
messages,
|
|
1655
|
+
contextTokens,
|
|
1656
|
+
reservedContextTokens
|
|
1657
|
+
}),
|
|
1658
|
+
checkpoint,
|
|
1659
|
+
totalContextTokens: contextTokens
|
|
1660
|
+
});
|
|
1661
|
+
};
|
|
1662
|
+
finish = async (pending) => {
|
|
1663
|
+
const compacted = await this.compactionService.compactPreparedForModelInput({
|
|
1664
|
+
contextTokens: pending.contextTokens,
|
|
1665
|
+
plan: pending.plan,
|
|
1666
|
+
generateSummary: async ({ messages }) => await this.generateSummary({
|
|
1667
|
+
messages,
|
|
1668
|
+
model: pending.model
|
|
1669
|
+
})
|
|
1670
|
+
});
|
|
1671
|
+
const generatedCheckpoint = compacted.checkpoint;
|
|
1672
|
+
if (!generatedCheckpoint) throw new Error("context compaction pending work did not produce a checkpoint");
|
|
1673
|
+
const checkpoint = {
|
|
1674
|
+
...generatedCheckpoint,
|
|
1675
|
+
id: pending.checkpoint.id,
|
|
1676
|
+
createdAt: pending.checkpoint.createdAt,
|
|
1677
|
+
status: "compressed"
|
|
1678
|
+
};
|
|
1679
|
+
const contextWindow = buildContextWindowSnapshotFromBudget({
|
|
1680
|
+
budget: this.contextWindowBudgetService.evaluate({
|
|
1681
|
+
messages: compacted.messages,
|
|
1682
|
+
contextTokens: pending.contextTokens,
|
|
1683
|
+
reservedContextTokens: pending.reservedContextTokens
|
|
1684
|
+
}),
|
|
1685
|
+
checkpoint,
|
|
1686
|
+
totalContextTokens: pending.contextTokens
|
|
1687
|
+
});
|
|
1688
|
+
const session = this.options.sessionManager.getOrCreate(pending.sessionId);
|
|
1689
|
+
session.metadata[CONTEXT_COMPACTION_METADATA_KEY] = checkpoint;
|
|
1690
|
+
upsertContextCompactionTimelineMessage({
|
|
1691
|
+
session,
|
|
1692
|
+
checkpoint,
|
|
1693
|
+
insertBeforeMessageIds: pending.inputMessageIds
|
|
1694
|
+
});
|
|
1695
|
+
this.options.sessionManager.save(session);
|
|
1696
|
+
return {
|
|
1697
|
+
contextWindow,
|
|
1698
|
+
metadata: structuredClone(session.metadata),
|
|
1699
|
+
sessionMessages: toNcpMessages(pending.sessionId, session.messages),
|
|
1700
|
+
timelineMessage: buildContextCompactionTimelineNcpMessage({
|
|
1701
|
+
sessionId: pending.sessionId,
|
|
1702
|
+
checkpoint
|
|
1703
|
+
})
|
|
1704
|
+
};
|
|
1705
|
+
};
|
|
1706
|
+
generateSummary = async (params) => {
|
|
1707
|
+
if (!this.options.providerManager) throw new Error("context compaction summary generation requires a provider manager");
|
|
1708
|
+
const { messages, model } = params;
|
|
1709
|
+
const summary = (await this.options.providerManager.chat({
|
|
1710
|
+
model,
|
|
1711
|
+
maxTokens: SUMMARY_MAX_TOKENS,
|
|
1712
|
+
messages: [{
|
|
1713
|
+
role: "system",
|
|
1714
|
+
content: [
|
|
1715
|
+
"You are NextClaw's context compactor for a coding agent session.",
|
|
1716
|
+
"Create a dense, structured summary that will replace earlier conversation history in a future model request.",
|
|
1717
|
+
"Preserve user goals, explicit instructions, decisions, files touched or inspected, code changes, commands run, test results, failures, blockers, and exact next steps.",
|
|
1718
|
+
"Do not invent facts. If something is uncertain, mark it as uncertain.",
|
|
1719
|
+
"Return Markdown only. Start with '# Compressed Earlier Context'."
|
|
1720
|
+
].join("\n")
|
|
1721
|
+
}, {
|
|
1722
|
+
role: "user",
|
|
1723
|
+
content: [
|
|
1724
|
+
"Compress these earlier runtime messages into a reusable checkpoint summary.",
|
|
1725
|
+
"",
|
|
1726
|
+
"Messages JSON:",
|
|
1727
|
+
stringifyCompactionSource(messages)
|
|
1728
|
+
].join("\n")
|
|
1729
|
+
}]
|
|
1730
|
+
})).content?.trim();
|
|
1731
|
+
if (!summary) throw new Error("context compaction summary is empty");
|
|
1732
|
+
return summary;
|
|
1733
|
+
};
|
|
1734
|
+
};
|
|
1735
|
+
//#endregion
|
|
1736
|
+
//#region src/types/llm-usage.types.ts
|
|
1737
|
+
function createLlmUsageRecord(params) {
|
|
1738
|
+
const { observedAt, source, model, usage: rawUsage } = params;
|
|
1739
|
+
const usage = sanitizeLlmUsage(rawUsage);
|
|
1740
|
+
return {
|
|
1741
|
+
version: 1,
|
|
1742
|
+
observedAt: observedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1743
|
+
source,
|
|
1744
|
+
model: normalizeLlmUsageModel(model),
|
|
1745
|
+
usage,
|
|
1746
|
+
summary: buildLlmUsageSummary(usage)
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
function sanitizeLlmUsage(usage) {
|
|
1750
|
+
const next = {};
|
|
1751
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
1752
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) continue;
|
|
1753
|
+
next[key] = Math.floor(value);
|
|
1754
|
+
}
|
|
1755
|
+
return next;
|
|
1756
|
+
}
|
|
1757
|
+
function hasLlmUsageTelemetry(usage) {
|
|
1758
|
+
return Object.keys(usage).length > 0;
|
|
1759
|
+
}
|
|
1760
|
+
function buildLlmUsageSummary(usage) {
|
|
1761
|
+
const promptTokens = usage.prompt_tokens ?? usage.input_tokens ?? 0;
|
|
1762
|
+
const completionTokens = usage.completion_tokens ?? usage.output_tokens ?? 0;
|
|
1763
|
+
const totalTokens = usage.total_tokens ?? promptTokens + completionTokens;
|
|
1764
|
+
const cacheMetricKeys = Object.keys(usage).filter((key) => key.endsWith("cached_tokens"));
|
|
1765
|
+
const cachedTokens = cacheMetricKeys.reduce((max, key) => Math.max(max, usage[key] ?? 0), 0);
|
|
1766
|
+
return {
|
|
1767
|
+
promptTokens,
|
|
1768
|
+
completionTokens,
|
|
1769
|
+
totalTokens,
|
|
1770
|
+
cachedTokens,
|
|
1771
|
+
cacheHit: cachedTokens > 0,
|
|
1772
|
+
cacheMetricKeys
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
function normalizeLlmUsageModel(value) {
|
|
1776
|
+
if (typeof value !== "string") return null;
|
|
1777
|
+
const trimmed = value.trim();
|
|
1778
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1779
|
+
}
|
|
1780
|
+
//#endregion
|
|
1781
|
+
//#region src/stores/llm-usage.store.ts
|
|
1782
|
+
var LlmUsageStore = class {
|
|
1783
|
+
constructor(options = {}) {
|
|
1784
|
+
this.options = options;
|
|
1785
|
+
}
|
|
1786
|
+
get snapshotPath() {
|
|
1787
|
+
return this.options.snapshotPath ?? resolve(getDataDir(), "run", "llm-usage.json");
|
|
1788
|
+
}
|
|
1789
|
+
get historyPath() {
|
|
1790
|
+
return this.options.historyPath ?? resolve(getDataDir(), "logs", "llm-usage.jsonl");
|
|
1791
|
+
}
|
|
1792
|
+
readSnapshot = () => {
|
|
1793
|
+
if (!existsSync(this.snapshotPath)) return null;
|
|
1794
|
+
try {
|
|
1795
|
+
const raw = readFileSync(this.snapshotPath, "utf-8");
|
|
1796
|
+
return JSON.parse(raw);
|
|
1797
|
+
} catch {
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1801
|
+
writeSnapshot = (snapshot) => {
|
|
1802
|
+
mkdirSync(dirname(this.snapshotPath), { recursive: true });
|
|
1803
|
+
writeFileSync(this.snapshotPath, JSON.stringify(snapshot, null, 2));
|
|
1804
|
+
};
|
|
1805
|
+
clearSnapshot = () => {
|
|
1806
|
+
if (existsSync(this.snapshotPath)) rmSync(this.snapshotPath, { force: true });
|
|
1807
|
+
};
|
|
1808
|
+
listHistory = () => {
|
|
1809
|
+
if (!existsSync(this.historyPath)) return [];
|
|
1810
|
+
try {
|
|
1811
|
+
return readFileSync(this.historyPath, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
|
|
1812
|
+
try {
|
|
1813
|
+
return [JSON.parse(line)];
|
|
1814
|
+
} catch {
|
|
1815
|
+
return [];
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1818
|
+
} catch {
|
|
1819
|
+
return [];
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
appendHistory = (record) => {
|
|
1823
|
+
mkdirSync(dirname(this.historyPath), { recursive: true });
|
|
1824
|
+
appendFileSync(this.historyPath, `${JSON.stringify(record)}\n`);
|
|
1825
|
+
};
|
|
1826
|
+
clearHistory = () => {
|
|
1827
|
+
if (existsSync(this.historyPath)) rmSync(this.historyPath, { force: true });
|
|
1828
|
+
};
|
|
1829
|
+
};
|
|
1830
|
+
//#endregion
|
|
1831
|
+
//#region src/managers/llm-usage.manager.ts
|
|
1832
|
+
var ObservedLlmProviderRuntime = class {
|
|
1833
|
+
constructor(delegate, manager, source) {
|
|
1834
|
+
this.delegate = delegate;
|
|
1835
|
+
this.manager = manager;
|
|
1836
|
+
this.source = source;
|
|
1837
|
+
}
|
|
1838
|
+
get = (model) => {
|
|
1839
|
+
return this.delegate.get(model);
|
|
1840
|
+
};
|
|
1841
|
+
chat = async (params) => {
|
|
1842
|
+
const response = await this.delegate.chat(params);
|
|
1843
|
+
this.manager.record({
|
|
1844
|
+
source: this.source,
|
|
1845
|
+
model: params.model ?? null,
|
|
1846
|
+
usage: response.usage
|
|
1847
|
+
});
|
|
1848
|
+
return response;
|
|
1849
|
+
};
|
|
1850
|
+
chatStream = async function* (params) {
|
|
1851
|
+
for await (const event of this.delegate.chatStream(params)) {
|
|
1852
|
+
if (event.type === "done") this.manager.record({
|
|
1853
|
+
source: this.source,
|
|
1854
|
+
model: params.model ?? null,
|
|
1855
|
+
usage: event.response.usage
|
|
1856
|
+
});
|
|
1857
|
+
yield event;
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1860
|
+
};
|
|
1861
|
+
var LlmUsageManager = class {
|
|
1862
|
+
store;
|
|
1863
|
+
constructor(options = {}) {
|
|
1864
|
+
this.store = options.store ?? new LlmUsageStore();
|
|
1865
|
+
}
|
|
1866
|
+
observeProviderManager = (providerManager, source) => {
|
|
1867
|
+
return new ObservedLlmProviderRuntime(providerManager, this, source);
|
|
1868
|
+
};
|
|
1869
|
+
record = (params) => {
|
|
1870
|
+
const record = createLlmUsageRecord(params);
|
|
1871
|
+
if (!hasLlmUsageTelemetry(record.usage)) return null;
|
|
1872
|
+
this.store.writeSnapshot(record);
|
|
1873
|
+
this.store.appendHistory(record);
|
|
1874
|
+
return record;
|
|
1875
|
+
};
|
|
1876
|
+
};
|
|
1877
|
+
//#endregion
|
|
1878
|
+
//#region src/tools/session-search.tools.ts
|
|
1879
|
+
function isRecord$2(value) {
|
|
1880
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1881
|
+
}
|
|
1882
|
+
function readOptionalInteger(value) {
|
|
1883
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return;
|
|
1884
|
+
return Math.trunc(value);
|
|
1885
|
+
}
|
|
1886
|
+
function readOptionalBoolean(value) {
|
|
1887
|
+
return typeof value === "boolean" ? value : void 0;
|
|
1888
|
+
}
|
|
1889
|
+
var SessionSearchTool = class {
|
|
1890
|
+
name = "session_search";
|
|
1891
|
+
description = "Search prior sessions by keyword and return structured hits with snippets. Use it to recall earlier discussions before creating new plans or summaries.";
|
|
1892
|
+
parameters = {
|
|
1893
|
+
type: "object",
|
|
1894
|
+
properties: {
|
|
1895
|
+
query: {
|
|
1896
|
+
type: "string",
|
|
1897
|
+
description: "Keyword search query for prior session text or labels."
|
|
1898
|
+
},
|
|
1899
|
+
limit: {
|
|
1900
|
+
type: "integer",
|
|
1901
|
+
minimum: 1,
|
|
1902
|
+
maximum: MAX_SESSION_SEARCH_LIMIT,
|
|
1903
|
+
description: `Optional max hit count. Defaults to ${DEFAULT_SESSION_SEARCH_LIMIT}.`
|
|
1904
|
+
},
|
|
1905
|
+
includeCurrentSession: {
|
|
1906
|
+
type: "boolean",
|
|
1907
|
+
description: "Set true to include the current session in results. Defaults to false."
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
required: ["query"],
|
|
1911
|
+
additionalProperties: false
|
|
1912
|
+
};
|
|
1913
|
+
constructor(queryService, context) {
|
|
1914
|
+
this.queryService = queryService;
|
|
1915
|
+
this.context = context;
|
|
1916
|
+
}
|
|
1917
|
+
validateArgs = (args) => {
|
|
1918
|
+
const issues = [];
|
|
1919
|
+
const query = normalizeString(args.query);
|
|
1920
|
+
const limit = readOptionalInteger(args.limit);
|
|
1921
|
+
const includeCurrentSession = readOptionalBoolean(args.includeCurrentSession);
|
|
1922
|
+
if (!query) issues.push("query must be a non-empty string.");
|
|
1923
|
+
if (typeof args.limit !== "undefined" && typeof limit === "undefined") issues.push("limit must be a finite integer.");
|
|
1924
|
+
if (typeof limit === "number" && (limit < 1 || limit > MAX_SESSION_SEARCH_LIMIT)) issues.push(`limit must be between 1 and ${MAX_SESSION_SEARCH_LIMIT}.`);
|
|
1925
|
+
if (typeof args.includeCurrentSession !== "undefined" && typeof includeCurrentSession === "undefined") issues.push("includeCurrentSession must be a boolean.");
|
|
1926
|
+
return issues;
|
|
1927
|
+
};
|
|
1928
|
+
execute = async (args) => {
|
|
1929
|
+
if (!isRecord$2(args)) throw new Error("session_search requires an object argument.");
|
|
1930
|
+
const issues = this.validateArgs(args);
|
|
1931
|
+
if (issues.length > 0) throw new Error(issues.join(" "));
|
|
1932
|
+
return this.queryService.search({
|
|
1933
|
+
query: normalizeString(args.query) ?? "",
|
|
1934
|
+
limit: readOptionalInteger(args.limit),
|
|
1935
|
+
includeCurrentSession: readOptionalBoolean(args.includeCurrentSession),
|
|
1936
|
+
currentSessionId: this.context.currentSessionId
|
|
1937
|
+
});
|
|
1938
|
+
};
|
|
1939
|
+
};
|
|
1940
|
+
//#endregion
|
|
1941
|
+
//#region src/features/native-runtime/tools/ncp-asset.tools.ts
|
|
1942
|
+
function readOptionalString$2(value) {
|
|
1943
|
+
if (typeof value !== "string") return null;
|
|
1944
|
+
const trimmed = value.trim();
|
|
1945
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1946
|
+
}
|
|
1947
|
+
function readOptionalBase64Bytes(value) {
|
|
1948
|
+
const base64 = readOptionalString$2(value);
|
|
1949
|
+
if (!base64) return null;
|
|
1950
|
+
try {
|
|
1951
|
+
return Buffer.from(base64, "base64");
|
|
1952
|
+
} catch {
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function toAssetPayload(record, contentBasePath) {
|
|
1957
|
+
return {
|
|
1958
|
+
uri: record.uri,
|
|
1959
|
+
name: record.fileName,
|
|
1960
|
+
mimeType: record.mimeType,
|
|
1961
|
+
sizeBytes: record.sizeBytes,
|
|
1962
|
+
createdAt: record.createdAt,
|
|
1963
|
+
url: buildAssetContentPath({
|
|
1964
|
+
basePath: contentBasePath,
|
|
1965
|
+
assetUri: record.uri
|
|
1966
|
+
})
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
var AssetPutTool = class {
|
|
1970
|
+
name = "asset_put";
|
|
1971
|
+
description = "Put a normal file path or base64 bytes into the managed asset store.";
|
|
1972
|
+
parameters = {
|
|
1973
|
+
type: "object",
|
|
1974
|
+
properties: {
|
|
1975
|
+
path: {
|
|
1976
|
+
type: "string",
|
|
1977
|
+
description: "Existing local file path to put into the asset store."
|
|
1978
|
+
},
|
|
1979
|
+
bytesBase64: {
|
|
1980
|
+
type: "string",
|
|
1981
|
+
description: "Base64 file bytes. Use together with fileName when no source path exists."
|
|
1982
|
+
},
|
|
1983
|
+
fileName: {
|
|
1984
|
+
type: "string",
|
|
1985
|
+
description: "Optional asset file name override. Required when using bytesBase64."
|
|
1986
|
+
},
|
|
1987
|
+
mimeType: {
|
|
1988
|
+
type: "string",
|
|
1989
|
+
description: "Optional mime type override."
|
|
1990
|
+
}
|
|
1991
|
+
},
|
|
1992
|
+
additionalProperties: false
|
|
1993
|
+
};
|
|
1994
|
+
constructor(assetStore, contentBasePath) {
|
|
1995
|
+
this.assetStore = assetStore;
|
|
1996
|
+
this.contentBasePath = contentBasePath;
|
|
1997
|
+
}
|
|
1998
|
+
validateArgs = (args) => {
|
|
1999
|
+
const path = readOptionalString$2(args.path);
|
|
2000
|
+
const bytesBase64 = readOptionalString$2(args.bytesBase64);
|
|
2001
|
+
const fileName = readOptionalString$2(args.fileName);
|
|
2002
|
+
if (path && bytesBase64) return ["Provide either path, or bytesBase64 + fileName, not both."];
|
|
2003
|
+
if (path) return [];
|
|
2004
|
+
if (bytesBase64) return fileName ? [] : ["fileName is required when using bytesBase64."];
|
|
2005
|
+
return ["Provide either path, or bytesBase64 + fileName."];
|
|
2006
|
+
};
|
|
2007
|
+
execute = async (args) => {
|
|
2008
|
+
const path = readOptionalString$2(args?.path);
|
|
2009
|
+
const fileName = readOptionalString$2(args?.fileName);
|
|
2010
|
+
const mimeType = readOptionalString$2(args?.mimeType);
|
|
2011
|
+
const bytes = readOptionalBase64Bytes(args?.bytesBase64);
|
|
2012
|
+
if (path) return {
|
|
2013
|
+
ok: true,
|
|
2014
|
+
asset: toAssetPayload(await this.assetStore.putPath({
|
|
2015
|
+
path,
|
|
2016
|
+
fileName: fileName ?? void 0,
|
|
2017
|
+
mimeType
|
|
2018
|
+
}), this.contentBasePath)
|
|
2019
|
+
};
|
|
2020
|
+
if (bytes && fileName) return {
|
|
2021
|
+
ok: true,
|
|
2022
|
+
asset: toAssetPayload(await this.assetStore.putBytes({
|
|
2023
|
+
fileName,
|
|
2024
|
+
mimeType,
|
|
2025
|
+
bytes
|
|
2026
|
+
}), this.contentBasePath)
|
|
2027
|
+
};
|
|
2028
|
+
throw new Error("asset_put received invalid arguments after validation.");
|
|
2029
|
+
};
|
|
2030
|
+
};
|
|
2031
|
+
var AssetExportTool = class {
|
|
2032
|
+
name = "asset_export";
|
|
2033
|
+
description = "Export a managed asset to a normal file path so it can be processed like any ordinary file.";
|
|
2034
|
+
parameters = {
|
|
2035
|
+
type: "object",
|
|
2036
|
+
properties: {
|
|
2037
|
+
assetUri: {
|
|
2038
|
+
type: "string",
|
|
2039
|
+
description: "Managed asset URI to export."
|
|
2040
|
+
},
|
|
2041
|
+
targetPath: {
|
|
2042
|
+
type: "string",
|
|
2043
|
+
description: "Destination file path."
|
|
2044
|
+
}
|
|
2045
|
+
},
|
|
2046
|
+
required: ["assetUri", "targetPath"],
|
|
2047
|
+
additionalProperties: false
|
|
2048
|
+
};
|
|
2049
|
+
constructor(assetStore) {
|
|
2050
|
+
this.assetStore = assetStore;
|
|
2051
|
+
}
|
|
2052
|
+
execute = async (args) => {
|
|
2053
|
+
const assetUri = readOptionalString$2(args?.assetUri);
|
|
2054
|
+
const targetPath = readOptionalString$2(args?.targetPath);
|
|
2055
|
+
if (!assetUri || !targetPath) throw new Error("asset_export requires assetUri and targetPath.");
|
|
2056
|
+
return {
|
|
2057
|
+
ok: true,
|
|
2058
|
+
assetUri,
|
|
2059
|
+
exportedPath: await this.assetStore.export({ uri: assetUri }, resolve(targetPath))
|
|
2060
|
+
};
|
|
2061
|
+
};
|
|
2062
|
+
};
|
|
2063
|
+
var AssetStatTool = class {
|
|
2064
|
+
name = "asset_stat";
|
|
2065
|
+
description = "Read managed asset metadata.";
|
|
2066
|
+
parameters = {
|
|
2067
|
+
type: "object",
|
|
2068
|
+
properties: { assetUri: {
|
|
2069
|
+
type: "string",
|
|
2070
|
+
description: "Managed asset URI to inspect."
|
|
2071
|
+
} },
|
|
2072
|
+
required: ["assetUri"],
|
|
2073
|
+
additionalProperties: false
|
|
2074
|
+
};
|
|
2075
|
+
constructor(assetStore, contentBasePath) {
|
|
2076
|
+
this.assetStore = assetStore;
|
|
2077
|
+
this.contentBasePath = contentBasePath;
|
|
2078
|
+
}
|
|
2079
|
+
execute = async (args) => {
|
|
2080
|
+
const assetUri = readOptionalString$2(args?.assetUri);
|
|
2081
|
+
if (!assetUri) throw new Error("asset_stat requires assetUri.");
|
|
2082
|
+
const record = await this.assetStore.statRecord(assetUri);
|
|
2083
|
+
if (!record) return {
|
|
2084
|
+
ok: false,
|
|
2085
|
+
error: {
|
|
2086
|
+
code: "not_found",
|
|
2087
|
+
message: `Asset not found: ${assetUri}`
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
return {
|
|
2091
|
+
ok: true,
|
|
2092
|
+
asset: toAssetPayload(record, this.contentBasePath)
|
|
2093
|
+
};
|
|
2094
|
+
};
|
|
2095
|
+
};
|
|
2096
|
+
function createAssetTools(params) {
|
|
2097
|
+
const { assetStore } = params;
|
|
2098
|
+
const contentBasePath = params.contentBasePath ?? "/api/ncp/assets/content";
|
|
2099
|
+
return [
|
|
2100
|
+
new AssetPutTool(assetStore, contentBasePath),
|
|
2101
|
+
new AssetExportTool(assetStore),
|
|
2102
|
+
new AssetStatTool(assetStore, contentBasePath)
|
|
2103
|
+
];
|
|
2104
|
+
}
|
|
2105
|
+
//#endregion
|
|
2106
|
+
//#region src/features/native-runtime/utils/nextclaw-ncp-session-preferences.utils.ts
|
|
2107
|
+
function normalizeOptionalString(value) {
|
|
2108
|
+
if (typeof value !== "string") return;
|
|
2109
|
+
const trimmed = value.trim();
|
|
2110
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
2111
|
+
}
|
|
2112
|
+
function readMetadataModel(metadata) {
|
|
2113
|
+
const candidates = [
|
|
2114
|
+
metadata.model,
|
|
2115
|
+
metadata.llm_model,
|
|
2116
|
+
metadata.agent_model,
|
|
2117
|
+
metadata.session_model
|
|
2118
|
+
];
|
|
2119
|
+
for (const candidate of candidates) {
|
|
2120
|
+
const normalized = normalizeOptionalString(candidate);
|
|
2121
|
+
if (normalized) return normalized;
|
|
2122
|
+
}
|
|
2123
|
+
return null;
|
|
2124
|
+
}
|
|
2125
|
+
function readMetadataThinking(metadata) {
|
|
2126
|
+
const candidates = [
|
|
2127
|
+
metadata.thinking,
|
|
2128
|
+
metadata.thinking_level,
|
|
2129
|
+
metadata.thinkingLevel,
|
|
2130
|
+
metadata.thinking_effort,
|
|
2131
|
+
metadata.thinkingEffort
|
|
2132
|
+
];
|
|
2133
|
+
for (const candidate of candidates) {
|
|
2134
|
+
if (typeof candidate !== "string") continue;
|
|
2135
|
+
const normalized = candidate.trim().toLowerCase();
|
|
2136
|
+
if (!normalized) continue;
|
|
2137
|
+
if (normalized === "clear" || normalized === "reset" || normalized === "off!") return "__clear__";
|
|
2138
|
+
const level = parseThinkingLevel(normalized);
|
|
2139
|
+
if (level) return level;
|
|
2140
|
+
}
|
|
2141
|
+
return null;
|
|
2142
|
+
}
|
|
2143
|
+
function resolveEffectiveModel(params) {
|
|
2144
|
+
const { fallbackModel, requestMetadata, session } = params;
|
|
2145
|
+
const metadata = session.metadata;
|
|
2146
|
+
if (requestMetadata.clear_model === true || requestMetadata.reset_model === true) delete metadata.preferred_model;
|
|
2147
|
+
const inboundModel = readMetadataModel(requestMetadata);
|
|
2148
|
+
if (inboundModel) metadata.preferred_model = inboundModel;
|
|
2149
|
+
return normalizeOptionalString(metadata.preferred_model) ?? fallbackModel;
|
|
2150
|
+
}
|
|
2151
|
+
function syncSessionThinkingPreference(params) {
|
|
2152
|
+
const { requestMetadata, session } = params;
|
|
2153
|
+
const metadata = session.metadata;
|
|
2154
|
+
if (requestMetadata.clear_thinking === true || requestMetadata.reset_thinking === true) delete metadata.preferred_thinking;
|
|
2155
|
+
const inboundThinking = readMetadataThinking(requestMetadata);
|
|
2156
|
+
if (inboundThinking === "__clear__") {
|
|
2157
|
+
delete metadata.preferred_thinking;
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
if (inboundThinking) metadata.preferred_thinking = inboundThinking;
|
|
2161
|
+
}
|
|
2162
|
+
function resolveSessionChannelContext(params) {
|
|
2163
|
+
const { requestMetadata, session } = params;
|
|
2164
|
+
const metadata = session.metadata;
|
|
2165
|
+
const channel = normalizeOptionalString(requestMetadata.channel) ?? normalizeOptionalString(metadata.last_channel) ?? "ui";
|
|
2166
|
+
const chatId = normalizeOptionalString(requestMetadata.chatId) ?? normalizeOptionalString(requestMetadata.chat_id) ?? normalizeOptionalString(metadata.last_to) ?? "web-ui";
|
|
2167
|
+
metadata.last_channel = channel;
|
|
2168
|
+
metadata.last_to = chatId;
|
|
2169
|
+
return {
|
|
2170
|
+
channel,
|
|
2171
|
+
chatId
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
//#endregion
|
|
2175
|
+
//#region src/features/native-runtime/utils/nextclaw-ncp-current-turn.utils.ts
|
|
2176
|
+
function isTextLikePart(part) {
|
|
2177
|
+
return part.type === "text" || part.type === "rich-text";
|
|
2178
|
+
}
|
|
2179
|
+
function collectTextPartIndexes(parts) {
|
|
2180
|
+
return parts.map((part, index) => isTextLikePart(part) ? index : -1).filter((index) => index >= 0);
|
|
2181
|
+
}
|
|
2182
|
+
function replaceTextPartsWithSingleFormattedText(parts, textPartIndexes, formattedText) {
|
|
2183
|
+
const nextParts = structuredClone(parts);
|
|
2184
|
+
const firstTextIndex = textPartIndexes[0];
|
|
2185
|
+
const firstTextPart = nextParts[firstTextIndex];
|
|
2186
|
+
if (firstTextPart && isTextLikePart(firstTextPart)) firstTextPart.text = formattedText;
|
|
2187
|
+
return nextParts.filter((part, index) => {
|
|
2188
|
+
if (!isTextLikePart(part)) return true;
|
|
2189
|
+
return index === firstTextIndex && part.text.length > 0;
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
function wrapTextPartsWithFormattedEdges(params) {
|
|
2193
|
+
const { parts, prefix, suffix, textPartIndexes } = params;
|
|
2194
|
+
const nextParts = structuredClone(parts);
|
|
2195
|
+
const firstTextIndex = textPartIndexes[0];
|
|
2196
|
+
const lastTextIndex = textPartIndexes[textPartIndexes.length - 1];
|
|
2197
|
+
for (const index of textPartIndexes) {
|
|
2198
|
+
const part = nextParts[index];
|
|
2199
|
+
if (!part || !isTextLikePart(part)) continue;
|
|
2200
|
+
if (index === firstTextIndex) part.text = `${prefix}${part.text}`;
|
|
2201
|
+
if (index === lastTextIndex) part.text = `${part.text}${suffix}`;
|
|
2202
|
+
}
|
|
2203
|
+
return nextParts;
|
|
2204
|
+
}
|
|
2205
|
+
function buildFormattedCurrentParts(params) {
|
|
2206
|
+
const { formattedText, message, originalText } = params;
|
|
2207
|
+
const parts = message?.parts ?? [];
|
|
2208
|
+
if (parts.length === 0) return formattedText.length > 0 ? [{
|
|
2209
|
+
type: "text",
|
|
2210
|
+
text: formattedText
|
|
2211
|
+
}] : [];
|
|
2212
|
+
const textPartIndexes = collectTextPartIndexes(parts);
|
|
2213
|
+
if (textPartIndexes.length === 0) return formattedText.length > 0 ? [{
|
|
2214
|
+
type: "text",
|
|
2215
|
+
text: formattedText
|
|
2216
|
+
}, ...structuredClone(parts)] : structuredClone(parts);
|
|
2217
|
+
if (formattedText === originalText) return structuredClone(parts);
|
|
2218
|
+
const originalTextIndex = formattedText.indexOf(originalText);
|
|
2219
|
+
if (originalText.length === 0 || originalTextIndex < 0) return replaceTextPartsWithSingleFormattedText(parts, textPartIndexes, formattedText);
|
|
2220
|
+
return wrapTextPartsWithFormattedEdges({
|
|
2221
|
+
parts,
|
|
2222
|
+
textPartIndexes,
|
|
2223
|
+
prefix: formattedText.slice(0, originalTextIndex),
|
|
2224
|
+
suffix: formattedText.slice(originalTextIndex + originalText.length)
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
function findLatestUserMessage(input) {
|
|
2228
|
+
return input.messages.slice().reverse().find((message) => message.role === "user") ?? input.messages[input.messages.length - 1];
|
|
2229
|
+
}
|
|
2230
|
+
function buildCurrentTurnState(params) {
|
|
2231
|
+
const { assetStore, currentModel, formatPrompt, input } = params;
|
|
2232
|
+
const latestUserMessage = findLatestUserMessage(input);
|
|
2233
|
+
const originalText = extractTextFromNcpMessage(latestUserMessage);
|
|
2234
|
+
const currentParts = buildFormattedCurrentParts({
|
|
2235
|
+
message: latestUserMessage,
|
|
2236
|
+
formattedText: formatPrompt({
|
|
2237
|
+
text: originalText,
|
|
2238
|
+
timestamp: new Date(ensureIsoTimestamp(latestUserMessage?.timestamp, (/* @__PURE__ */ new Date()).toISOString()))
|
|
2239
|
+
}),
|
|
2240
|
+
originalText
|
|
2241
|
+
});
|
|
2242
|
+
return {
|
|
2243
|
+
currentRole: latestUserMessage?.role === "system" ? "system" : "user",
|
|
2244
|
+
currentUserContent: buildLegacyUserContent(currentParts, { assetStore }),
|
|
2245
|
+
effectiveModel: currentModel
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
//#endregion
|
|
2249
|
+
//#region src/tools/session-request.tools.ts
|
|
2250
|
+
function readRequiredString$1(params, key) {
|
|
2251
|
+
const value = params[key];
|
|
2252
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${key} must be a non-empty string.`);
|
|
2253
|
+
return value.trim();
|
|
2254
|
+
}
|
|
2255
|
+
function readOptionalString$1(params, key) {
|
|
2256
|
+
const value = params[key];
|
|
2257
|
+
if (typeof value !== "string") return;
|
|
2258
|
+
const trimmed = value.trim();
|
|
2259
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
2260
|
+
}
|
|
2261
|
+
var SessionRequestTool = class extends Tool {
|
|
2262
|
+
sourceSessionId = "";
|
|
2263
|
+
handoffDepth = 0;
|
|
2264
|
+
constructor(manager) {
|
|
2265
|
+
super();
|
|
2266
|
+
this.manager = manager;
|
|
2267
|
+
}
|
|
2268
|
+
get name() {
|
|
2269
|
+
return "sessions_request";
|
|
2270
|
+
}
|
|
2271
|
+
get description() {
|
|
2272
|
+
return "Send one task to another session. Use notify to control whether this session should continue after the target session finishes.";
|
|
2273
|
+
}
|
|
2274
|
+
get parameters() {
|
|
2275
|
+
return {
|
|
2276
|
+
type: "object",
|
|
2277
|
+
properties: {
|
|
2278
|
+
target: {
|
|
2279
|
+
type: "object",
|
|
2280
|
+
description: "Target session reference. Pass an object like {\"session_id\":\"...\"}, not a bare string.",
|
|
2281
|
+
properties: { session_id: {
|
|
2282
|
+
type: "string",
|
|
2283
|
+
description: "Existing target session id."
|
|
2284
|
+
} },
|
|
2285
|
+
required: ["session_id"]
|
|
2286
|
+
},
|
|
2287
|
+
task: {
|
|
2288
|
+
type: "string",
|
|
2289
|
+
description: "Task to send to the target session."
|
|
2290
|
+
},
|
|
2291
|
+
notify: {
|
|
2292
|
+
type: "string",
|
|
2293
|
+
enum: ["none", "final_reply"],
|
|
2294
|
+
description: "Whether the current session should continue after the target session finishes. Use \"final_reply\" to continue after the target session reaches its final reply."
|
|
2295
|
+
},
|
|
2296
|
+
title: {
|
|
2297
|
+
type: "string",
|
|
2298
|
+
description: "Optional card title override."
|
|
2299
|
+
}
|
|
2300
|
+
},
|
|
2301
|
+
required: [
|
|
2302
|
+
"target",
|
|
2303
|
+
"task",
|
|
2304
|
+
"notify"
|
|
2305
|
+
]
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
setContext = (params) => {
|
|
2309
|
+
this.sourceSessionId = params.sourceSessionId;
|
|
2310
|
+
this.handoffDepth = params.handoffDepth ?? 0;
|
|
2311
|
+
};
|
|
2312
|
+
execute = async (params, context) => {
|
|
2313
|
+
const target = params.target;
|
|
2314
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) throw new Error("target must be an object.");
|
|
2315
|
+
const task = readRequiredString$1(params, "task");
|
|
2316
|
+
const notifyMode = readOptionalString$1(params, "notify")?.toLowerCase();
|
|
2317
|
+
if (notifyMode !== "none" && notifyMode !== "final_reply") throw new Error("notify must be \"none\" or \"final_reply\".");
|
|
2318
|
+
const { toolCallId, updateToolCallResult } = context;
|
|
2319
|
+
return this.manager.requestSession({
|
|
2320
|
+
sourceSessionId: this.sourceSessionId,
|
|
2321
|
+
...toolCallId ? { sourceToolCallId: toolCallId } : {},
|
|
2322
|
+
updateToolCallResult,
|
|
2323
|
+
targetSessionId: readRequiredString$1(target, "session_id"),
|
|
2324
|
+
task,
|
|
2325
|
+
title: readOptionalString$1(params, "title"),
|
|
2326
|
+
notify: notifyMode,
|
|
2327
|
+
handoffDepth: this.handoffDepth
|
|
2328
|
+
});
|
|
2329
|
+
};
|
|
2330
|
+
};
|
|
2331
|
+
//#endregion
|
|
2332
|
+
//#region src/tools/session-spawn.tools.ts
|
|
2333
|
+
function readRequiredString(value, key) {
|
|
2334
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${key} must be a non-empty string.`);
|
|
2335
|
+
return value.trim();
|
|
2336
|
+
}
|
|
2337
|
+
function readOptionalString(value) {
|
|
2338
|
+
if (typeof value !== "string") return;
|
|
2339
|
+
const trimmed = value.trim();
|
|
2340
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
2341
|
+
}
|
|
2342
|
+
function readSpawnScope(value) {
|
|
2343
|
+
const normalized = readOptionalString(value)?.toLowerCase();
|
|
2344
|
+
if (!normalized || normalized === "standalone") return "standalone";
|
|
2345
|
+
if (normalized === "child") return "child";
|
|
2346
|
+
throw new Error("scope must be \"standalone\" or \"child\".");
|
|
2347
|
+
}
|
|
2348
|
+
function readSpawnRequestOptions(value) {
|
|
2349
|
+
if (typeof value === "undefined") return;
|
|
2350
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("request must be an object.");
|
|
2351
|
+
const notifyMode = readOptionalString(value.notify)?.toLowerCase();
|
|
2352
|
+
if (notifyMode === "none" || notifyMode === "final_reply") return { notify: notifyMode };
|
|
2353
|
+
throw new Error("request.notify must be \"none\" or \"final_reply\".");
|
|
2354
|
+
}
|
|
2355
|
+
var SessionSpawnTool = class extends Tool {
|
|
2356
|
+
sourceSessionId = "";
|
|
2357
|
+
sourceSessionMetadata = {};
|
|
2358
|
+
handoffDepth = 0;
|
|
2359
|
+
constructor(sessionManager, sessionRequestManager, onSessionUpdated) {
|
|
2360
|
+
super();
|
|
2361
|
+
this.sessionManager = sessionManager;
|
|
2362
|
+
this.sessionRequestManager = sessionRequestManager;
|
|
2363
|
+
this.onSessionUpdated = onSessionUpdated;
|
|
2364
|
+
}
|
|
2365
|
+
get name() {
|
|
2366
|
+
return "sessions_spawn";
|
|
2367
|
+
}
|
|
2368
|
+
get description() {
|
|
2369
|
+
return "Create a new session. Use scope=\"child\" to create a child session of the current flow, and add request.notify to control whether this session should continue after the new session finishes.";
|
|
2370
|
+
}
|
|
2371
|
+
get parameters() {
|
|
2372
|
+
return {
|
|
2373
|
+
type: "object",
|
|
2374
|
+
properties: {
|
|
2375
|
+
task: {
|
|
2376
|
+
type: "string",
|
|
2377
|
+
description: "Seed text used to title the new session. If request is provided, this same task is also sent as the first request to that new session."
|
|
2378
|
+
},
|
|
2379
|
+
scope: {
|
|
2380
|
+
type: "string",
|
|
2381
|
+
enum: ["standalone", "child"],
|
|
2382
|
+
description: "Whether the new session should be a standalone thread or a child session of the current session."
|
|
2383
|
+
},
|
|
2384
|
+
title: {
|
|
2385
|
+
type: "string",
|
|
2386
|
+
description: "Optional explicit session title."
|
|
2387
|
+
},
|
|
2388
|
+
model: {
|
|
2389
|
+
type: "string",
|
|
2390
|
+
description: "Optional model override for the new session."
|
|
2391
|
+
},
|
|
2392
|
+
runtime: {
|
|
2393
|
+
type: "string",
|
|
2394
|
+
description: "Optional runtime override for the new session, for example native or codex."
|
|
2395
|
+
},
|
|
2396
|
+
agentId: {
|
|
2397
|
+
type: "string",
|
|
2398
|
+
description: "Optional target agent id for the new session. Omit to use the default agent."
|
|
2399
|
+
},
|
|
2400
|
+
request: {
|
|
2401
|
+
type: "object",
|
|
2402
|
+
description: "Optional initial request to run immediately inside the new session.",
|
|
2403
|
+
properties: { notify: {
|
|
2404
|
+
type: "string",
|
|
2405
|
+
enum: ["none", "final_reply"],
|
|
2406
|
+
description: "Whether the current session should continue after the new session finishes. Use \"final_reply\" to continue after the new session reaches its final reply."
|
|
2407
|
+
} },
|
|
2408
|
+
required: ["notify"]
|
|
2409
|
+
}
|
|
2410
|
+
},
|
|
2411
|
+
required: ["task"]
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
setContext = (params) => {
|
|
2415
|
+
this.sourceSessionId = params.sourceSessionId;
|
|
2416
|
+
this.sourceSessionMetadata = structuredClone(params.sourceSessionMetadata);
|
|
2417
|
+
this.handoffDepth = params.handoffDepth ?? 0;
|
|
2418
|
+
};
|
|
2419
|
+
execute = async (params, context) => {
|
|
2420
|
+
const { agentId: rawAgentId, model: rawModel, request: rawRequest, runtime: rawRuntime, scope: rawScope, task: rawTask, title: rawTitle } = params;
|
|
2421
|
+
const task = readRequiredString(rawTask, "task");
|
|
2422
|
+
const scope = readSpawnScope(rawScope);
|
|
2423
|
+
const request = readSpawnRequestOptions(rawRequest);
|
|
2424
|
+
const parentSessionId = scope === "child" ? this.readParentSessionIdOrThrow() : void 0;
|
|
2425
|
+
const { toolCallId, updateToolCallResult } = context;
|
|
2426
|
+
if (request) return this.sessionRequestManager.spawnSessionAndRequest({
|
|
2427
|
+
sourceSessionId: this.sourceSessionId,
|
|
2428
|
+
...toolCallId ? { sourceToolCallId: toolCallId } : {},
|
|
2429
|
+
updateToolCallResult,
|
|
2430
|
+
sourceSessionMetadata: this.sourceSessionMetadata,
|
|
2431
|
+
task,
|
|
2432
|
+
title: readOptionalString(rawTitle),
|
|
2433
|
+
agentId: readOptionalString(rawAgentId),
|
|
2434
|
+
model: readOptionalString(rawModel),
|
|
2435
|
+
runtime: readOptionalString(rawRuntime),
|
|
2436
|
+
handoffDepth: this.handoffDepth,
|
|
2437
|
+
...parentSessionId ? { parentSessionId } : {},
|
|
2438
|
+
notify: request.notify
|
|
2439
|
+
});
|
|
2440
|
+
const session = this.sessionManager.createSession({
|
|
2441
|
+
sourceSessionId: this.sourceSessionId,
|
|
2442
|
+
task,
|
|
2443
|
+
title: readOptionalString(rawTitle),
|
|
2444
|
+
sourceSessionMetadata: this.sourceSessionMetadata,
|
|
2445
|
+
agentId: readOptionalString(rawAgentId),
|
|
2446
|
+
model: readOptionalString(rawModel),
|
|
2447
|
+
runtime: readOptionalString(rawRuntime),
|
|
2448
|
+
...parentSessionId ? { parentSessionId } : {}
|
|
2449
|
+
});
|
|
2450
|
+
this.onSessionUpdated?.(session.sessionId);
|
|
2451
|
+
return {
|
|
2452
|
+
kind: "nextclaw.session",
|
|
2453
|
+
sessionId: session.sessionId,
|
|
2454
|
+
...session.agentId ? { agentId: session.agentId } : {},
|
|
2455
|
+
...session.parentSessionId ? { parentSessionId: session.parentSessionId } : {},
|
|
2456
|
+
isChildSession: Boolean(session.parentSessionId),
|
|
2457
|
+
lifecycle: session.lifecycle,
|
|
2458
|
+
title: session.title,
|
|
2459
|
+
sessionType: session.sessionType,
|
|
2460
|
+
createdAt: session.createdAt
|
|
2461
|
+
};
|
|
2462
|
+
};
|
|
2463
|
+
readParentSessionIdOrThrow = () => {
|
|
2464
|
+
const sourceSessionId = this.sourceSessionId.trim();
|
|
2465
|
+
if (!sourceSessionId) throw new Error("scope=\"child\" requires an active source session.");
|
|
2466
|
+
return sourceSessionId;
|
|
2467
|
+
};
|
|
2468
|
+
};
|
|
2469
|
+
//#endregion
|
|
2470
|
+
//#region src/features/native-runtime/services/nextclaw-ncp-tool-registry.service.ts
|
|
2471
|
+
function toToolParams(args) {
|
|
2472
|
+
if (isRecord$3(args)) return args;
|
|
2473
|
+
if (typeof args === "string") try {
|
|
2474
|
+
const parsed = JSON.parse(args);
|
|
2475
|
+
return isRecord$3(parsed) ? parsed : {};
|
|
2476
|
+
} catch {
|
|
2477
|
+
return {};
|
|
2478
|
+
}
|
|
2479
|
+
return {};
|
|
2480
|
+
}
|
|
2481
|
+
function readMetadataAccountId(metadata, sessionMetadata) {
|
|
2482
|
+
const candidates = [
|
|
2483
|
+
metadata.accountId,
|
|
2484
|
+
metadata.account_id,
|
|
2485
|
+
sessionMetadata.last_account_id
|
|
2486
|
+
];
|
|
2487
|
+
for (const candidate of candidates) {
|
|
2488
|
+
const normalized = normalizeString(candidate);
|
|
2489
|
+
if (normalized) return normalized;
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
var CoreToolNcpAdapter = class {
|
|
2493
|
+
constructor(tool, executeTool) {
|
|
2494
|
+
this.tool = tool;
|
|
2495
|
+
this.executeTool = executeTool;
|
|
2496
|
+
}
|
|
2497
|
+
get name() {
|
|
2498
|
+
return this.tool.name;
|
|
2499
|
+
}
|
|
2500
|
+
get description() {
|
|
2501
|
+
return this.tool.description;
|
|
2502
|
+
}
|
|
2503
|
+
get parameters() {
|
|
2504
|
+
return this.tool.parameters;
|
|
2505
|
+
}
|
|
2506
|
+
execute = async (args) => {
|
|
2507
|
+
return this.executeTool(this.tool.name, args);
|
|
2508
|
+
};
|
|
2509
|
+
};
|
|
2510
|
+
var NextclawNcpToolRegistry = class {
|
|
2511
|
+
registry = new ToolRegistry();
|
|
2512
|
+
tools = /* @__PURE__ */ new Map();
|
|
2513
|
+
currentExtensionToolContext = {};
|
|
2514
|
+
constructor(options) {
|
|
2515
|
+
this.options = options;
|
|
2516
|
+
}
|
|
2517
|
+
prepareForRun = (context) => {
|
|
2518
|
+
const { channel, chatId, config, restrictToWorkspace, sessionId, workspace } = context;
|
|
2519
|
+
this.currentExtensionToolContext = {
|
|
2520
|
+
config,
|
|
2521
|
+
workspaceDir: workspace,
|
|
2522
|
+
sessionKey: sessionId,
|
|
2523
|
+
channel,
|
|
2524
|
+
chatId,
|
|
2525
|
+
sandboxed: restrictToWorkspace
|
|
2526
|
+
};
|
|
2527
|
+
this.registry = new ToolRegistry();
|
|
2528
|
+
this.tools.clear();
|
|
2529
|
+
this.registerDefaultTools(context);
|
|
2530
|
+
this.registerExtensionTools(context);
|
|
2531
|
+
this.registerAdditionalTools(context);
|
|
2532
|
+
};
|
|
2533
|
+
listTools = () => {
|
|
2534
|
+
return [...this.tools.values()].filter((tool) => this.isToolAvailable(tool.name));
|
|
37
2535
|
};
|
|
38
|
-
|
|
39
|
-
|
|
2536
|
+
getTool = (name) => {
|
|
2537
|
+
if (!this.isToolAvailable(name)) return;
|
|
2538
|
+
return this.tools.get(name);
|
|
40
2539
|
};
|
|
41
|
-
|
|
42
|
-
|
|
2540
|
+
getToolDefinitions = () => {
|
|
2541
|
+
return this.listTools().map((tool) => ({
|
|
2542
|
+
name: tool.name,
|
|
2543
|
+
description: tool.description,
|
|
2544
|
+
parameters: tool.parameters
|
|
2545
|
+
}));
|
|
43
2546
|
};
|
|
44
|
-
|
|
45
|
-
|
|
2547
|
+
execute = async (toolCallId, toolName, args) => {
|
|
2548
|
+
if (this.registry.has(toolName)) return this.registry.executeRaw(toolName, toToolParams(args), this.buildToolExecutionContext(toolCallId));
|
|
2549
|
+
return this.tools.get(toolName)?.execute(args);
|
|
46
2550
|
};
|
|
47
|
-
|
|
48
|
-
|
|
2551
|
+
buildToolExecutionContext = (toolCallId) => {
|
|
2552
|
+
const sessionId = this.currentExtensionToolContext.sessionKey;
|
|
2553
|
+
return createToolExecutionContext({
|
|
2554
|
+
toolCallId,
|
|
2555
|
+
updateToolCallResult: async (result) => {
|
|
2556
|
+
if (!sessionId) return;
|
|
2557
|
+
await this.options.updateToolCallResult({
|
|
2558
|
+
sessionId,
|
|
2559
|
+
toolCallId,
|
|
2560
|
+
result
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
});
|
|
49
2564
|
};
|
|
50
|
-
|
|
51
|
-
|
|
2565
|
+
registerDefaultTools = (context) => {
|
|
2566
|
+
const { channel, chatId, execTimeoutSeconds, handoffDepth, metadata, restrictToWorkspace, searchConfig, sessionId, workspace } = context;
|
|
2567
|
+
const allowedDir = restrictToWorkspace ? workspace : void 0;
|
|
2568
|
+
this.registerTool(new ReadFileTool(allowedDir));
|
|
2569
|
+
this.registerTool(new WriteFileTool(allowedDir));
|
|
2570
|
+
this.registerTool(new EditFileTool(allowedDir));
|
|
2571
|
+
this.registerTool(new ListDirTool(allowedDir));
|
|
2572
|
+
const execTool = new ExecTool({
|
|
2573
|
+
workingDir: workspace,
|
|
2574
|
+
timeout: execTimeoutSeconds,
|
|
2575
|
+
restrictToWorkspace
|
|
2576
|
+
});
|
|
2577
|
+
execTool.setContext({
|
|
2578
|
+
sessionKey: sessionId,
|
|
2579
|
+
channel,
|
|
2580
|
+
chatId
|
|
2581
|
+
});
|
|
2582
|
+
this.registerTool(execTool);
|
|
2583
|
+
this.registerTool(new WebSearchTool(searchConfig));
|
|
2584
|
+
this.registerTool(new WebFetchTool());
|
|
2585
|
+
this.registerMessagingTools({
|
|
2586
|
+
channel,
|
|
2587
|
+
chatId,
|
|
2588
|
+
metadata
|
|
2589
|
+
});
|
|
2590
|
+
const sessionsSpawnTool = new SessionSpawnTool(this.options.sessionManager, this.options.sessionRequestManager, this.options.onSessionUpdated);
|
|
2591
|
+
sessionsSpawnTool.setContext({
|
|
2592
|
+
sourceSessionId: sessionId,
|
|
2593
|
+
sourceSessionMetadata: metadata,
|
|
2594
|
+
handoffDepth
|
|
2595
|
+
});
|
|
2596
|
+
this.registerTool(sessionsSpawnTool);
|
|
2597
|
+
const sessionsRequestTool = new SessionRequestTool(this.options.sessionRequestManager);
|
|
2598
|
+
sessionsRequestTool.setContext({
|
|
2599
|
+
sourceSessionId: sessionId,
|
|
2600
|
+
handoffDepth
|
|
2601
|
+
});
|
|
2602
|
+
this.registerTool(sessionsRequestTool);
|
|
2603
|
+
this.registerTool(new SessionsListTool(this.options.sessionManager));
|
|
2604
|
+
this.registerTool(new SessionsHistoryTool(this.options.sessionManager));
|
|
2605
|
+
this.registerTool(new MemorySearchTool(workspace));
|
|
2606
|
+
this.registerTool(new MemoryGetTool(workspace));
|
|
2607
|
+
const gatewayTool = new GatewayTool(this.options.gatewayController);
|
|
2608
|
+
gatewayTool.setContext({ sessionKey: sessionId });
|
|
2609
|
+
this.registerTool(gatewayTool);
|
|
52
2610
|
};
|
|
53
|
-
|
|
54
|
-
|
|
2611
|
+
registerMessagingTools = (context) => {
|
|
2612
|
+
const { channel, chatId, metadata } = context;
|
|
2613
|
+
const accountId = readMetadataAccountId(metadata, {});
|
|
2614
|
+
const messageTool = new MessageTool((message) => this.options.bus.publishOutbound(message));
|
|
2615
|
+
messageTool.setContext(channel, chatId, accountId ?? null);
|
|
2616
|
+
this.registerTool(messageTool);
|
|
2617
|
+
if (this.options.cronService) {
|
|
2618
|
+
const cronTool = new CronTool(this.options.cronService);
|
|
2619
|
+
cronTool.setContext(channel, chatId, accountId ?? null);
|
|
2620
|
+
this.registerTool(cronTool);
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2623
|
+
registerExtensionTools = (context) => {
|
|
2624
|
+
const extensionRegistry = this.options.getExtensionRegistry?.();
|
|
2625
|
+
if (!extensionRegistry || extensionRegistry.tools.length === 0) return;
|
|
2626
|
+
const seen = new Set(this.registry.toolNames);
|
|
2627
|
+
for (const registration of extensionRegistry.tools) for (const alias of registration.names) {
|
|
2628
|
+
if (seen.has(alias)) continue;
|
|
2629
|
+
seen.add(alias);
|
|
2630
|
+
this.registerTool(new ExtensionToolAdapter({
|
|
2631
|
+
registration,
|
|
2632
|
+
alias,
|
|
2633
|
+
config: context.config,
|
|
2634
|
+
workspaceDir: context.workspace,
|
|
2635
|
+
contextProvider: () => this.currentExtensionToolContext,
|
|
2636
|
+
diagnostics: extensionRegistry.diagnostics
|
|
2637
|
+
}));
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
registerTool = (tool) => {
|
|
2641
|
+
this.registry.register(tool);
|
|
2642
|
+
this.tools.set(tool.name, new CoreToolNcpAdapter(tool, async (toolName, args) => this.registry.execute(toolName, toToolParams(args))));
|
|
2643
|
+
};
|
|
2644
|
+
registerAdditionalTools = (context) => {
|
|
2645
|
+
const tools = this.options.getAdditionalTools?.(context) ?? [];
|
|
2646
|
+
for (const tool of tools) {
|
|
2647
|
+
if (this.tools.has(tool.name)) continue;
|
|
2648
|
+
this.tools.set(tool.name, tool);
|
|
2649
|
+
}
|
|
2650
|
+
};
|
|
2651
|
+
isToolAvailable = (name) => {
|
|
2652
|
+
const coreTool = this.registry.get(name);
|
|
2653
|
+
return coreTool ? coreTool.isAvailable() : true;
|
|
2654
|
+
};
|
|
2655
|
+
};
|
|
2656
|
+
function resolveAgentHandoffDepth(metadata) {
|
|
2657
|
+
const rawDepth = Number(metadata.agent_handoff_depth ?? 0);
|
|
2658
|
+
if (!Number.isFinite(rawDepth) || rawDepth < 0) return 0;
|
|
2659
|
+
return Math.trunc(rawDepth);
|
|
2660
|
+
}
|
|
2661
|
+
function readAccountIdForHints(metadata, sessionMetadata) {
|
|
2662
|
+
return readMetadataAccountId(metadata, sessionMetadata);
|
|
2663
|
+
}
|
|
2664
|
+
//#endregion
|
|
2665
|
+
//#region src/features/native-runtime/services/nextclaw-ncp-context-builder.service.ts
|
|
2666
|
+
const TIME_HINT_TRIGGER_PATTERNS = [/\b(now|right now|current time|what time|today|tonight|tomorrow|yesterday|this morning|this afternoon|this evening|date)\b/i, /(现在|此刻|当前时间|现在几点|几点了|今天|今晚|今早|今晨|明天|昨天|日期)/];
|
|
2667
|
+
function isRecord$1(value) {
|
|
2668
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2669
|
+
}
|
|
2670
|
+
function mergeInputMetadata(input) {
|
|
2671
|
+
const messageMetadata = input.messages.slice().reverse().find((message) => isRecord$1(message.metadata))?.metadata;
|
|
2672
|
+
return {
|
|
2673
|
+
...isRecord$1(messageMetadata) ? structuredClone(messageMetadata) : {},
|
|
2674
|
+
...isRecord$1(input.metadata) ? structuredClone(input.metadata) : {}
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
const REQUESTED_SKILLS_METADATA_READER = new RequestedSkillsMetadataReader();
|
|
2678
|
+
function resolveRequestedToolNames(metadata) {
|
|
2679
|
+
const rawValue = metadata.requested_tools ?? metadata.requestedTools;
|
|
2680
|
+
if (!Array.isArray(rawValue)) return [];
|
|
2681
|
+
return Array.from(new Set(rawValue.map((item) => normalizeString(item)).filter((item) => Boolean(item))));
|
|
2682
|
+
}
|
|
2683
|
+
function readRequestedAgentId(metadata) {
|
|
2684
|
+
return normalizeString(metadata.agent_id)?.toLowerCase() ?? normalizeString(metadata.agentId)?.toLowerCase() ?? null;
|
|
2685
|
+
}
|
|
2686
|
+
function resolveAgentProfile(params) {
|
|
2687
|
+
const { config, requestMetadata, storedAgentId } = params;
|
|
2688
|
+
const { agents: { defaults }, search: searchConfig, tools: { restrictToWorkspace, exec: { timeout: execTimeoutSeconds } } } = config;
|
|
2689
|
+
const defaultAgentId = resolveDefaultAgentProfileId(config);
|
|
2690
|
+
const profile = findEffectiveAgentProfile(config, normalizeString(storedAgentId)?.toLowerCase() ?? readRequestedAgentId(requestMetadata) ?? defaultAgentId) ?? findEffectiveAgentProfile(config, defaultAgentId);
|
|
2691
|
+
if (!profile) throw new Error(`default agent profile not found: ${defaultAgentId}`);
|
|
2692
|
+
const contextTokens = profile.contextTokens ?? defaults.contextTokens;
|
|
2693
|
+
return {
|
|
2694
|
+
agentId: profile.id,
|
|
2695
|
+
workspace: getWorkspacePath(profile.workspace ?? defaults.workspace),
|
|
2696
|
+
model: profile.model ?? defaults.model,
|
|
2697
|
+
contextTokens,
|
|
2698
|
+
reservedContextTokens: ContextWindowBudgetService.resolveReservedContextTokens({
|
|
2699
|
+
contextTokens,
|
|
2700
|
+
configuredReservedContextTokens: profile.reservedContextTokens ?? defaults.reservedContextTokens
|
|
2701
|
+
}),
|
|
2702
|
+
restrictToWorkspace,
|
|
2703
|
+
searchConfig,
|
|
2704
|
+
execTimeoutSeconds
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
function shouldAppendTimeHint(content) {
|
|
2708
|
+
const normalized = content.trim();
|
|
2709
|
+
if (!normalized) return false;
|
|
2710
|
+
return TIME_HINT_TRIGGER_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
2711
|
+
}
|
|
2712
|
+
function buildMinutePrecisionTimeHint(date) {
|
|
2713
|
+
const year = date.getFullYear();
|
|
2714
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
2715
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
2716
|
+
const hour = String(date.getHours()).padStart(2, "0");
|
|
2717
|
+
const minute = String(date.getMinutes()).padStart(2, "0");
|
|
2718
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
2719
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
2720
|
+
const absMinutes = Math.abs(offsetMinutes);
|
|
2721
|
+
return `${year}-${month}-${day} ${hour}:${minute} ${sign}${String(Math.floor(absMinutes / 60)).padStart(2, "0")}:${String(absMinutes % 60).padStart(2, "0")} (${Intl.DateTimeFormat().resolvedOptions().timeZone || "local"})`;
|
|
2722
|
+
}
|
|
2723
|
+
function appendTimeHintForPrompt(content, timestamp) {
|
|
2724
|
+
if (!shouldAppendTimeHint(content)) return content;
|
|
2725
|
+
return `${content}\n\n[time_hint_local_minute] ${buildMinutePrecisionTimeHint(Number.isNaN(timestamp.getTime()) ? /* @__PURE__ */ new Date() : timestamp)}`;
|
|
2726
|
+
}
|
|
2727
|
+
function prependRequestedSkills(content, requestedSkillSelectors) {
|
|
2728
|
+
if (requestedSkillSelectors.length === 0) return content;
|
|
2729
|
+
return `[Requested skills for this turn: ${requestedSkillSelectors.join(", ")}]\n\n${content}`;
|
|
2730
|
+
}
|
|
2731
|
+
function buildSessionOrchestrationSection() {
|
|
2732
|
+
return [
|
|
2733
|
+
"## Session Orchestration",
|
|
2734
|
+
"- Before passing a non-default `runtime` to `sessions_spawn` or agent creation/update flows, inspect the installed runtime kinds with `nextclaw agents runtimes --json`.",
|
|
2735
|
+
"- `sessions_spawn` is the unified session-creation tool. Omit `scope` or use `scope=\"standalone\"` for a regular session, and use `scope=\"child\"` when the new session should be a child session of the current flow.",
|
|
2736
|
+
"- `sessions_spawn` only creates the session by default. Add `request: { notify: \"none\" | \"final_reply\" }` when the new session should start working immediately.",
|
|
2737
|
+
"- When `sessions_spawn.scope=\"child\"` and `sessions_spawn.request.notify=\"final_reply\"`, the new child session starts right away and this session automatically continues after that child reaches its final reply.",
|
|
2738
|
+
"- Use `sessions_spawn` without `request` when the user wants a separate thread created now but does not need it to start working yet.",
|
|
2739
|
+
"- Use `sessions_request` to send one task to an existing session, including a session that was just created by `sessions_spawn` or a previously created child session.",
|
|
2740
|
+
"- `sessions_request.target` must be an object shaped like `{ \"session_id\": \"<target-session-id>\" }`. Do not pass a bare string.",
|
|
2741
|
+
"- Prefer `notify=\"final_reply\"` when the current session should continue after the target session produces its final reply. Use `notify=\"none\"` when you only want the target session to run independently."
|
|
2742
|
+
].join("\n");
|
|
2743
|
+
}
|
|
2744
|
+
function buildRequestedOpenAiTools(toolDefinitions, requestedToolNames) {
|
|
2745
|
+
const tools = toolDefinitions.map(buildOpenAiFunctionTool);
|
|
2746
|
+
if (tools.length === 0) return;
|
|
2747
|
+
if (requestedToolNames.length === 0) return tools;
|
|
2748
|
+
const requested = new Set(requestedToolNames);
|
|
2749
|
+
const filtered = tools.filter((tool) => requested.has(tool.function.name));
|
|
2750
|
+
return filtered.length > 0 ? filtered : void 0;
|
|
2751
|
+
}
|
|
2752
|
+
var NextclawNcpContextBuilder = class {
|
|
2753
|
+
inputBudgetPruner = new InputBudgetPruner();
|
|
2754
|
+
constructor(options) {
|
|
2755
|
+
this.options = options;
|
|
2756
|
+
}
|
|
2757
|
+
prepare = (input, _options) => {
|
|
2758
|
+
const runContext = this.prepareRunContext(input);
|
|
2759
|
+
const modelMessages = this.buildModelMessages(input, _options, runContext);
|
|
2760
|
+
return {
|
|
2761
|
+
messages: this.pruneModelMessages(runContext, modelMessages).messages,
|
|
2762
|
+
tools: buildRequestedOpenAiTools(modelMessages.toolDefinitions, runContext.requestedToolNames),
|
|
2763
|
+
model: runContext.effectiveModel,
|
|
2764
|
+
thinkingLevel: runContext.runtimeThinking
|
|
2765
|
+
};
|
|
2766
|
+
};
|
|
2767
|
+
prepareRunContext = (input) => {
|
|
2768
|
+
const config = this.options.getConfig();
|
|
2769
|
+
const requestMetadata = mergeInputMetadata(input);
|
|
2770
|
+
const session = this.options.sessionManager.getOrCreate(input.sessionId);
|
|
2771
|
+
const profile = resolveAgentProfile({
|
|
2772
|
+
config,
|
|
2773
|
+
storedAgentId: session.agentId,
|
|
2774
|
+
requestMetadata
|
|
2775
|
+
});
|
|
2776
|
+
let effectiveModel = resolveEffectiveModel({
|
|
2777
|
+
session,
|
|
2778
|
+
requestMetadata,
|
|
2779
|
+
fallbackModel: profile.model
|
|
2780
|
+
});
|
|
2781
|
+
const effectiveWorkspace = resolveSessionWorkspacePath({
|
|
2782
|
+
sessionMetadata: session.metadata,
|
|
2783
|
+
workspace: profile.workspace
|
|
2784
|
+
});
|
|
2785
|
+
syncSessionThinkingPreference({
|
|
2786
|
+
session,
|
|
2787
|
+
requestMetadata
|
|
2788
|
+
});
|
|
2789
|
+
const { channel, chatId } = resolveSessionChannelContext({
|
|
2790
|
+
session,
|
|
2791
|
+
requestMetadata
|
|
2792
|
+
});
|
|
2793
|
+
const requestedSkills = REQUESTED_SKILLS_METADATA_READER.readSelection(requestMetadata);
|
|
2794
|
+
const currentTurn = buildCurrentTurnState({
|
|
2795
|
+
input,
|
|
2796
|
+
currentModel: effectiveModel,
|
|
2797
|
+
formatPrompt: ({ text, timestamp }) => appendTimeHintForPrompt(prependRequestedSkills(text, requestedSkills.selectors), timestamp),
|
|
2798
|
+
assetStore: this.options.assetStore
|
|
2799
|
+
});
|
|
2800
|
+
effectiveModel = currentTurn.effectiveModel;
|
|
2801
|
+
const runtimeThinking = resolveThinkingLevel({
|
|
2802
|
+
config,
|
|
2803
|
+
agentId: profile.agentId,
|
|
2804
|
+
model: effectiveModel,
|
|
2805
|
+
sessionThinkingLevel: parseThinkingLevel(session.metadata.preferred_thinking) ?? null
|
|
2806
|
+
});
|
|
2807
|
+
this.options.toolRegistry.prepareForRun({
|
|
2808
|
+
sessionId: input.sessionId,
|
|
2809
|
+
channel,
|
|
2810
|
+
chatId,
|
|
2811
|
+
agentId: profile.agentId,
|
|
2812
|
+
config,
|
|
2813
|
+
contextTokens: profile.contextTokens,
|
|
2814
|
+
execTimeoutSeconds: profile.execTimeoutSeconds,
|
|
2815
|
+
handoffDepth: resolveAgentHandoffDepth(requestMetadata),
|
|
2816
|
+
metadata: requestMetadata,
|
|
2817
|
+
model: effectiveModel,
|
|
2818
|
+
restrictToWorkspace: profile.restrictToWorkspace,
|
|
2819
|
+
searchConfig: profile.searchConfig,
|
|
2820
|
+
workspace: effectiveWorkspace
|
|
2821
|
+
});
|
|
2822
|
+
return {
|
|
2823
|
+
accountId: readAccountIdForHints(requestMetadata, session.metadata),
|
|
2824
|
+
channel,
|
|
2825
|
+
chatId,
|
|
2826
|
+
config,
|
|
2827
|
+
currentTurn,
|
|
2828
|
+
effectiveModel,
|
|
2829
|
+
effectiveWorkspace,
|
|
2830
|
+
profile,
|
|
2831
|
+
requestMetadata,
|
|
2832
|
+
requestedSkills,
|
|
2833
|
+
requestedToolNames: resolveRequestedToolNames(requestMetadata),
|
|
2834
|
+
runtimeThinking,
|
|
2835
|
+
session,
|
|
2836
|
+
sessionKey: input.sessionId
|
|
2837
|
+
};
|
|
2838
|
+
};
|
|
2839
|
+
buildModelMessages = (input, _options, runContext) => {
|
|
2840
|
+
const { accountId, channel, chatId, config, currentTurn, effectiveModel, effectiveWorkspace, profile, requestedSkills, runtimeThinking, session } = runContext;
|
|
2841
|
+
const messageToolHints = this.options.resolveMessageToolHints?.({
|
|
2842
|
+
sessionKey: input.sessionId,
|
|
2843
|
+
channel,
|
|
2844
|
+
chatId,
|
|
2845
|
+
accountId: accountId ?? null
|
|
2846
|
+
});
|
|
2847
|
+
const toolDefinitions = this.options.toolRegistry.getToolDefinitions();
|
|
2848
|
+
const additionalSystemSections = [buildSessionOrchestrationSection(), buildMinimalSystemExecutionPrompt(effectiveModel)].filter(Boolean);
|
|
2849
|
+
const contextBuilder = new ContextBuilder(effectiveWorkspace, config.agents.context, {
|
|
2850
|
+
hostWorkspace: profile.workspace,
|
|
2851
|
+
sessionProjectRoot: readSessionProjectRoot(session.metadata)
|
|
2852
|
+
});
|
|
2853
|
+
const sessionMessages = projectNcpMessagesWithContextCompaction({
|
|
2854
|
+
sessionId: input.sessionId,
|
|
2855
|
+
sessionMessages: _options?.sessionMessages ?? []
|
|
2856
|
+
});
|
|
2857
|
+
const messages = contextBuilder.buildMessages({
|
|
2858
|
+
history: toLegacyMessages([...sessionMessages], {
|
|
2859
|
+
assetStore: this.options.assetStore,
|
|
2860
|
+
serviceRole: "system"
|
|
2861
|
+
}),
|
|
2862
|
+
currentMessage: "",
|
|
2863
|
+
channel,
|
|
2864
|
+
chatId,
|
|
2865
|
+
sessionKey: input.sessionId,
|
|
2866
|
+
thinkingLevel: runtimeThinking,
|
|
2867
|
+
skillNames: requestedSkills.selectors,
|
|
2868
|
+
messageToolHints,
|
|
2869
|
+
availableTools: buildToolCatalogEntries(toolDefinitions),
|
|
2870
|
+
additionalSystemSections
|
|
2871
|
+
});
|
|
2872
|
+
messages[messages.length - 1] = {
|
|
2873
|
+
role: currentTurn.currentRole,
|
|
2874
|
+
content: currentTurn.currentUserContent
|
|
2875
|
+
};
|
|
2876
|
+
return {
|
|
2877
|
+
messages,
|
|
2878
|
+
toolDefinitions
|
|
2879
|
+
};
|
|
2880
|
+
};
|
|
2881
|
+
pruneModelMessages = (runContext, modelMessages) => {
|
|
2882
|
+
return this.inputBudgetPruner.prune({
|
|
2883
|
+
messages: modelMessages.messages,
|
|
2884
|
+
contextTokens: runContext.profile.contextTokens,
|
|
2885
|
+
reserveTokensFloor: runContext.profile.reservedContextTokens,
|
|
2886
|
+
softThresholdTokens: 0
|
|
2887
|
+
});
|
|
55
2888
|
};
|
|
56
2889
|
};
|
|
57
2890
|
//#endregion
|
|
58
|
-
//#region src/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
2891
|
+
//#region src/features/native-runtime/services/provider-manager-ncp-llm-api.service.ts
|
|
2892
|
+
function normalizeModel(value) {
|
|
2893
|
+
if (typeof value !== "string") return null;
|
|
2894
|
+
const trimmed = value.trim();
|
|
2895
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
2896
|
+
}
|
|
2897
|
+
function normalizeThinkingLevel(value) {
|
|
2898
|
+
return parseThinkingLevel(value);
|
|
2899
|
+
}
|
|
2900
|
+
function normalizeFinishReason(value) {
|
|
2901
|
+
if (typeof value !== "string") return "stop";
|
|
2902
|
+
const trimmed = value.trim();
|
|
2903
|
+
return trimmed.length > 0 ? trimmed : "stop";
|
|
2904
|
+
}
|
|
2905
|
+
function toToolCallDelta(toolCall, index) {
|
|
2906
|
+
return {
|
|
2907
|
+
index,
|
|
2908
|
+
id: toolCall.id,
|
|
2909
|
+
type: "function",
|
|
2910
|
+
function: {
|
|
2911
|
+
name: toolCall.name,
|
|
2912
|
+
arguments: JSON.stringify(toolCall.arguments ?? {})
|
|
2913
|
+
}
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
function toFinalChunk(response, options) {
|
|
2917
|
+
const delta = {};
|
|
2918
|
+
if (options.includeText && typeof response.content === "string" && response.content.length > 0) delta.content = response.content;
|
|
2919
|
+
if (options.includeReasoning && typeof response.reasoningContent === "string" && response.reasoningContent.length > 0) delta.reasoning_content = response.reasoningContent;
|
|
2920
|
+
if (options.includeToolCalls && response.toolCalls.length > 0) delta.tool_calls = response.toolCalls.map((toolCall, index) => toToolCallDelta(toolCall, index));
|
|
2921
|
+
return {
|
|
2922
|
+
choices: [{
|
|
2923
|
+
delta,
|
|
2924
|
+
finish_reason: normalizeFinishReason(response.finishReason)
|
|
2925
|
+
}],
|
|
2926
|
+
usage: response.usage
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
var ProviderManagerNcpLLMApi = class {
|
|
2930
|
+
constructor(providerManager) {
|
|
2931
|
+
this.providerManager = providerManager;
|
|
2932
|
+
}
|
|
2933
|
+
generate = async function* (input, options) {
|
|
2934
|
+
const model = normalizeModel(input.model) ?? this.providerManager.get(null).getDefaultModel();
|
|
2935
|
+
const thinkingLevel = normalizeThinkingLevel(input.thinkingLevel);
|
|
2936
|
+
let sawTextDelta = false;
|
|
2937
|
+
let sawReasoningDelta = false;
|
|
2938
|
+
let sawToolCallDelta = false;
|
|
2939
|
+
for await (const event of this.providerManager.chatStream({
|
|
2940
|
+
messages: input.messages,
|
|
2941
|
+
tools: input.tools,
|
|
2942
|
+
model,
|
|
2943
|
+
...thinkingLevel ? { thinkingLevel } : {},
|
|
2944
|
+
maxTokens: input.max_tokens,
|
|
2945
|
+
signal: options?.signal
|
|
2946
|
+
})) {
|
|
2947
|
+
if (event.type === "delta") {
|
|
2948
|
+
sawTextDelta = true;
|
|
2949
|
+
yield { choices: [{ delta: { content: event.delta } }] };
|
|
2950
|
+
continue;
|
|
2951
|
+
}
|
|
2952
|
+
if (event.type === "reasoning_delta") {
|
|
2953
|
+
sawReasoningDelta = true;
|
|
2954
|
+
yield { choices: [{ delta: { reasoning_content: event.delta } }] };
|
|
2955
|
+
continue;
|
|
2956
|
+
}
|
|
2957
|
+
if (event.type === "tool_call_delta") {
|
|
2958
|
+
sawToolCallDelta = true;
|
|
2959
|
+
yield { choices: [{ delta: { tool_calls: event.toolCalls } }] };
|
|
2960
|
+
continue;
|
|
2961
|
+
}
|
|
2962
|
+
yield toFinalChunk(event.response, {
|
|
2963
|
+
includeText: !sawTextDelta,
|
|
2964
|
+
includeReasoning: !sawReasoningDelta,
|
|
2965
|
+
includeToolCalls: !sawToolCallDelta
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
62
2968
|
};
|
|
63
|
-
|
|
64
|
-
|
|
2969
|
+
};
|
|
2970
|
+
//#endregion
|
|
2971
|
+
//#region src/features/native-runtime/services/native-agent-runtime-factory.service.ts
|
|
2972
|
+
function createContextWindowUpdatedEvent(params) {
|
|
2973
|
+
return {
|
|
2974
|
+
type: NcpEventType.ContextWindowUpdated,
|
|
2975
|
+
payload: {
|
|
2976
|
+
sessionId: params.sessionId,
|
|
2977
|
+
contextWindow: params.contextWindow
|
|
2978
|
+
}
|
|
65
2979
|
};
|
|
66
|
-
|
|
67
|
-
|
|
2980
|
+
}
|
|
2981
|
+
function isRecord(value) {
|
|
2982
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2983
|
+
}
|
|
2984
|
+
function resolveNativeReasoningNormalizationMode(params) {
|
|
2985
|
+
const runtimeEntry = params.config.agents.runtimes.entries.native?.config ?? params.config.ui.ncp.runtimes.native;
|
|
2986
|
+
const runtimeMetadata = isRecord(runtimeEntry) ? runtimeEntry : {};
|
|
2987
|
+
return readAssistantReasoningNormalizationModeFromMetadata(params.sessionMetadata) ?? readAssistantReasoningNormalizationMode(runtimeMetadata.reasoningNormalization) ?? readAssistantReasoningNormalizationMode(runtimeMetadata.reasoning_normalization) ?? readAssistantReasoningNormalizationMode(runtimeMetadata.reasoningNormalizationMode) ?? readAssistantReasoningNormalizationMode(runtimeMetadata.reasoning_normalization_mode) ?? "think-tags";
|
|
2988
|
+
}
|
|
2989
|
+
function createSessionSearchTools(params) {
|
|
2990
|
+
const { currentSessionId, sessionSearch } = params;
|
|
2991
|
+
return sessionSearch.isReady() ? [new SessionSearchTool({ search: sessionSearch.search }, { currentSessionId })] : [];
|
|
2992
|
+
}
|
|
2993
|
+
async function* publishPreflightResult(params) {
|
|
2994
|
+
const { input, result, stateManager } = params;
|
|
2995
|
+
const contextWindowEvent = createContextWindowUpdatedEvent({
|
|
2996
|
+
contextWindow: result.contextWindow,
|
|
2997
|
+
sessionId: input.sessionId
|
|
2998
|
+
});
|
|
2999
|
+
await stateManager.dispatch(contextWindowEvent);
|
|
3000
|
+
yield contextWindowEvent;
|
|
3001
|
+
if (!result.timelineMessage) return;
|
|
3002
|
+
const activeRun = stateManager.getSnapshot().activeRun;
|
|
3003
|
+
stateManager.hydrate({
|
|
3004
|
+
sessionId: input.sessionId,
|
|
3005
|
+
messages: result.sessionMessages,
|
|
3006
|
+
activeRun,
|
|
3007
|
+
contextWindow: result.contextWindow
|
|
3008
|
+
});
|
|
3009
|
+
const timelineEvent = {
|
|
3010
|
+
type: NcpEventType.MessageSent,
|
|
3011
|
+
payload: {
|
|
3012
|
+
sessionId: input.sessionId,
|
|
3013
|
+
message: result.timelineMessage
|
|
3014
|
+
}
|
|
68
3015
|
};
|
|
69
|
-
|
|
70
|
-
|
|
3016
|
+
await stateManager.dispatch(timelineEvent);
|
|
3017
|
+
yield timelineEvent;
|
|
3018
|
+
}
|
|
3019
|
+
var NativeAgentRuntimeFactory = class {
|
|
3020
|
+
observedProviderManager;
|
|
3021
|
+
constructor(options) {
|
|
3022
|
+
this.options = options;
|
|
3023
|
+
this.observedProviderManager = options.llmUsage.observeProviderManager(options.providerManager, "ui-ncp");
|
|
3024
|
+
}
|
|
3025
|
+
create = ({ stateManager, sessionMetadata, setSessionMetadata }) => {
|
|
3026
|
+
const reasoningNormalizationMode = resolveNativeReasoningNormalizationMode({
|
|
3027
|
+
config: this.options.configManager.loadConfig(),
|
|
3028
|
+
sessionMetadata
|
|
3029
|
+
});
|
|
3030
|
+
if (reasoningNormalizationMode !== "off" && readAssistantReasoningNormalizationModeFromMetadata(sessionMetadata) !== reasoningNormalizationMode) setSessionMetadata(writeAssistantReasoningNormalizationModeToMetadata(sessionMetadata, reasoningNormalizationMode));
|
|
3031
|
+
const toolRegistry = this.createToolRegistry(this.observedProviderManager);
|
|
3032
|
+
const runtime = new DefaultNcpAgentRuntime({
|
|
3033
|
+
contextBuilder: new NextclawNcpContextBuilder({
|
|
3034
|
+
sessionManager: this.options.sessions,
|
|
3035
|
+
toolRegistry,
|
|
3036
|
+
getConfig: this.options.configManager.loadConfig,
|
|
3037
|
+
resolveMessageToolHints: ({ channel, accountId }) => this.options.extensions.resolveMessageToolHints({
|
|
3038
|
+
channel,
|
|
3039
|
+
config: this.options.configManager.loadConfig(),
|
|
3040
|
+
accountId
|
|
3041
|
+
}),
|
|
3042
|
+
assetStore: this.options.assetStore
|
|
3043
|
+
}),
|
|
3044
|
+
llmApi: new ProviderManagerNcpLLMApi(this.observedProviderManager),
|
|
3045
|
+
toolRegistry,
|
|
3046
|
+
stateManager,
|
|
3047
|
+
reasoningNormalizationMode
|
|
3048
|
+
});
|
|
3049
|
+
const contextCompactionPreflight = new ContextCompactionPreflightService({
|
|
3050
|
+
getConfig: this.options.configManager.loadConfig,
|
|
3051
|
+
providerManager: this.observedProviderManager,
|
|
3052
|
+
sessionManager: this.options.sessions
|
|
3053
|
+
});
|
|
3054
|
+
const { onSessionUpdated } = this.options;
|
|
3055
|
+
return { run: async function* (input, options) {
|
|
3056
|
+
const beginResult = contextCompactionPreflight.begin({
|
|
3057
|
+
contextWindowOwner: "nextclaw",
|
|
3058
|
+
inputMessages: input.messages,
|
|
3059
|
+
requestMetadata: input.metadata ?? {},
|
|
3060
|
+
sessionId: input.sessionId,
|
|
3061
|
+
sessionMessages: stateManager.getSnapshot().messages
|
|
3062
|
+
});
|
|
3063
|
+
if (beginResult) {
|
|
3064
|
+
setSessionMetadata(beginResult.metadata);
|
|
3065
|
+
onSessionUpdated(input.sessionId);
|
|
3066
|
+
yield* publishPreflightResult({
|
|
3067
|
+
input,
|
|
3068
|
+
result: beginResult,
|
|
3069
|
+
stateManager
|
|
3070
|
+
});
|
|
3071
|
+
if (beginResult.pendingCompaction) {
|
|
3072
|
+
const finishResult = await contextCompactionPreflight.finish(beginResult.pendingCompaction);
|
|
3073
|
+
setSessionMetadata(finishResult.metadata);
|
|
3074
|
+
onSessionUpdated(input.sessionId);
|
|
3075
|
+
yield* publishPreflightResult({
|
|
3076
|
+
input,
|
|
3077
|
+
result: finishResult,
|
|
3078
|
+
stateManager
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
yield* runtime.run(input, options);
|
|
3083
|
+
} };
|
|
71
3084
|
};
|
|
72
|
-
|
|
73
|
-
|
|
3085
|
+
resolveOpenAiToolsForRuntime = (input) => {
|
|
3086
|
+
return new NextclawNcpContextBuilder({
|
|
3087
|
+
sessionManager: this.options.sessions,
|
|
3088
|
+
toolRegistry: this.createToolRegistry(this.options.providerManager),
|
|
3089
|
+
getConfig: this.options.configManager.loadConfig,
|
|
3090
|
+
resolveMessageToolHints: ({ channel, accountId }) => this.options.extensions.resolveMessageToolHints({
|
|
3091
|
+
channel,
|
|
3092
|
+
config: this.options.configManager.loadConfig(),
|
|
3093
|
+
accountId
|
|
3094
|
+
}),
|
|
3095
|
+
assetStore: this.options.assetStore
|
|
3096
|
+
}).prepare(input).tools;
|
|
74
3097
|
};
|
|
75
|
-
|
|
76
|
-
|
|
3098
|
+
createToolRegistry = (providerManager) => {
|
|
3099
|
+
return new NextclawNcpToolRegistry({
|
|
3100
|
+
bus: this.options.bus,
|
|
3101
|
+
providerManager,
|
|
3102
|
+
sessionManager: this.options.sessions,
|
|
3103
|
+
cronService: this.options.cronService,
|
|
3104
|
+
gatewayController: this.options.resolveGatewayController(),
|
|
3105
|
+
getConfig: this.options.configManager.loadConfig,
|
|
3106
|
+
getExtensionRegistry: this.options.extensions.getExtensionRegistry,
|
|
3107
|
+
onSessionUpdated: this.options.onSessionUpdated,
|
|
3108
|
+
sessionRequestManager: this.options.sessionRequests,
|
|
3109
|
+
updateToolCallResult: this.options.updateToolCallResult,
|
|
3110
|
+
getAdditionalTools: (context) => [
|
|
3111
|
+
...createAssetTools({ assetStore: this.options.assetStore }),
|
|
3112
|
+
...this.options.mcpToolRegistryAdapter.listToolsForRun({ agentId: context.agentId }),
|
|
3113
|
+
...createSessionSearchTools({
|
|
3114
|
+
sessionSearch: this.options.sessionSearch,
|
|
3115
|
+
currentSessionId: context.sessionId
|
|
3116
|
+
})
|
|
3117
|
+
]
|
|
3118
|
+
});
|
|
77
3119
|
};
|
|
78
3120
|
};
|
|
79
3121
|
//#endregion
|
|
80
|
-
//#region src/
|
|
81
|
-
var
|
|
82
|
-
|
|
83
|
-
|
|
3122
|
+
//#region src/features/mcp-runtime-support/services/mcp-runtime-support.service.ts
|
|
3123
|
+
var McpRuntimeSupportOwner = class {
|
|
3124
|
+
currentMcpConfig;
|
|
3125
|
+
mcpLifecycleManager;
|
|
3126
|
+
mcpRegistryService;
|
|
3127
|
+
toolRegistryAdapter;
|
|
3128
|
+
constructor(getConfig) {
|
|
3129
|
+
this.currentMcpConfig = getConfig();
|
|
3130
|
+
this.mcpLifecycleManager = new McpServerLifecycleManager({ getConfig: () => this.currentMcpConfig });
|
|
3131
|
+
this.mcpRegistryService = new McpRegistryService({
|
|
3132
|
+
getConfig: () => this.currentMcpConfig,
|
|
3133
|
+
lifecycleManager: this.mcpLifecycleManager
|
|
3134
|
+
});
|
|
3135
|
+
this.toolRegistryAdapter = new McpNcpToolRegistryAdapter(this.mcpRegistryService);
|
|
84
3136
|
}
|
|
85
|
-
|
|
86
|
-
this.
|
|
87
|
-
|
|
3137
|
+
applyMcpConfig = async (config) => {
|
|
3138
|
+
const previousConfig = this.currentMcpConfig;
|
|
3139
|
+
this.currentMcpConfig = config;
|
|
3140
|
+
const reconcileResult = await this.mcpRegistryService.reconcileConfig({
|
|
3141
|
+
prevConfig: previousConfig,
|
|
3142
|
+
nextConfig: config
|
|
3143
|
+
});
|
|
3144
|
+
for (const warmResult of reconcileResult.warmed) if (!warmResult.ok) console.warn(`[mcp] Failed to warm ${warmResult.name}: ${warmResult.error}`);
|
|
3145
|
+
};
|
|
3146
|
+
prewarmEnabledServers = async () => await this.mcpRegistryService.prewarmEnabledServers();
|
|
3147
|
+
dispose = async () => {
|
|
3148
|
+
await this.mcpRegistryService.close();
|
|
88
3149
|
};
|
|
89
3150
|
};
|
|
90
3151
|
//#endregion
|
|
91
|
-
//#region src/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
3152
|
+
//#region src/features/session-request/utils/agent-runtime-session-request-dispatcher.utils.ts
|
|
3153
|
+
const AGENT_RUNTIME_SESSION_MESSAGE_INGRESS_TYPE = createTypedKey("agent-runtime.session-message.request");
|
|
3154
|
+
function extractSessionMessageText(message) {
|
|
3155
|
+
const parts = message.parts.flatMap((part) => part.type === "text" || part.type === "rich-text" ? [part.text] : []).map((part) => part.trim()).filter((part) => part.length > 0);
|
|
3156
|
+
return parts.length > 0 ? parts.join("\n\n") : void 0;
|
|
3157
|
+
}
|
|
3158
|
+
function waitForAgentRuntimeSessionReply(input) {
|
|
3159
|
+
let acceptedMessageId = null;
|
|
3160
|
+
let unsubscribe = () => void 0;
|
|
3161
|
+
const promise = new Promise((resolve, reject) => {
|
|
3162
|
+
unsubscribe = input.eventBus.on(eventKeys.ncpEvent, (event) => {
|
|
3163
|
+
switch (event.type) {
|
|
3164
|
+
case NcpEventType.MessageAccepted:
|
|
3165
|
+
if (event.payload.correlationId === input.requestId) {
|
|
3166
|
+
acceptedMessageId = event.payload.messageId;
|
|
3167
|
+
input.onAccepted(event.payload.messageId);
|
|
3168
|
+
}
|
|
3169
|
+
return;
|
|
3170
|
+
case NcpEventType.MessageCompleted:
|
|
3171
|
+
if (event.payload.correlationId === input.requestId || event.payload.message.id === acceptedMessageId) {
|
|
3172
|
+
unsubscribe();
|
|
3173
|
+
resolve(event.payload.message);
|
|
3174
|
+
}
|
|
3175
|
+
return;
|
|
3176
|
+
case NcpEventType.MessageFailed:
|
|
3177
|
+
if (event.payload.correlationId === input.requestId || event.payload.messageId === acceptedMessageId) {
|
|
3178
|
+
unsubscribe();
|
|
3179
|
+
reject(new Error(event.payload.error.message));
|
|
3180
|
+
}
|
|
3181
|
+
return;
|
|
3182
|
+
case NcpEventType.RunError:
|
|
3183
|
+
if (event.payload.messageId === acceptedMessageId) {
|
|
3184
|
+
unsubscribe();
|
|
3185
|
+
reject(new Error(event.payload.error ?? "Session request failed."));
|
|
3186
|
+
}
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
});
|
|
3190
|
+
});
|
|
3191
|
+
return {
|
|
3192
|
+
dispose: unsubscribe,
|
|
3193
|
+
promise
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
async function dispatchAgentRuntimeSessionRequest(input) {
|
|
3197
|
+
await input.ingress.handle({
|
|
3198
|
+
type: AGENT_RUNTIME_SESSION_MESSAGE_INGRESS_TYPE,
|
|
3199
|
+
payload: {
|
|
3200
|
+
message: {
|
|
3201
|
+
id: `${input.request.targetSessionId}:user:session-request:${input.request.requestId}`,
|
|
3202
|
+
sessionId: input.request.targetSessionId,
|
|
3203
|
+
role: "user",
|
|
3204
|
+
status: "final",
|
|
3205
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3206
|
+
parts: [{
|
|
3207
|
+
type: "text",
|
|
3208
|
+
text: input.task
|
|
3209
|
+
}],
|
|
3210
|
+
metadata: { session_request_id: input.request.requestId }
|
|
3211
|
+
},
|
|
3212
|
+
requestId: input.request.requestId,
|
|
3213
|
+
sessionId: input.request.targetSessionId
|
|
3214
|
+
}
|
|
3215
|
+
}, { source: "session-request" });
|
|
3216
|
+
}
|
|
3217
|
+
function createAgentRuntimeSessionRequestDispatcher(options) {
|
|
3218
|
+
return { dispatch: async (input) => {
|
|
3219
|
+
const reply = waitForAgentRuntimeSessionReply({
|
|
3220
|
+
eventBus: options.eventBus,
|
|
3221
|
+
onAccepted: input.onAccepted,
|
|
3222
|
+
requestId: input.request.requestId
|
|
3223
|
+
});
|
|
3224
|
+
try {
|
|
3225
|
+
await dispatchAgentRuntimeSessionRequest({
|
|
3226
|
+
ingress: options.ingress,
|
|
3227
|
+
request: input.request,
|
|
3228
|
+
task: input.task
|
|
3229
|
+
});
|
|
3230
|
+
const message = await reply.promise;
|
|
3231
|
+
return {
|
|
3232
|
+
finalResponseMessageId: message.id,
|
|
3233
|
+
finalResponseText: extractSessionMessageText(message)
|
|
3234
|
+
};
|
|
3235
|
+
} catch (error) {
|
|
3236
|
+
reply.dispose();
|
|
3237
|
+
throw error;
|
|
3238
|
+
}
|
|
3239
|
+
} };
|
|
3240
|
+
}
|
|
3241
|
+
//#endregion
|
|
3242
|
+
//#region src/managers/agent-runtime.manager.ts
|
|
3243
|
+
var AgentRuntimeManager = class {
|
|
3244
|
+
runtimeRegistry = new AgentRuntimeRegistry();
|
|
3245
|
+
mcpRuntimeSupport;
|
|
3246
|
+
assetStore = new LocalAssetStore({ rootDir: join(getDataDir(), "assets") });
|
|
3247
|
+
pluginRuntimeRegistrationController;
|
|
3248
|
+
refreshConfiguredRuntimeEntries = () => {
|
|
3249
|
+
this.runtimeRegistry.applyEntries(resolveAgentRuntimeEntries({
|
|
3250
|
+
config: this.params.configManager.loadConfig(),
|
|
3251
|
+
providerKinds: this.runtimeRegistry.listProviderKinds()
|
|
3252
|
+
}));
|
|
3253
|
+
};
|
|
3254
|
+
backend = null;
|
|
3255
|
+
handle = null;
|
|
3256
|
+
builtinNarpRegistrations = [];
|
|
3257
|
+
kernelBootstrapped = false;
|
|
3258
|
+
warmupPromise = null;
|
|
3259
|
+
unsubscribeNcpEvents = null;
|
|
3260
|
+
unsubscribeAgentRuntimeRequestIngress = null;
|
|
3261
|
+
gatewayController;
|
|
3262
|
+
disposed = false;
|
|
3263
|
+
constructor(params) {
|
|
3264
|
+
this.params = params;
|
|
3265
|
+
this.mcpRuntimeSupport = new McpRuntimeSupportOwner(params.configManager.loadConfig);
|
|
3266
|
+
this.pluginRuntimeRegistrationController = new PluginRuntimeRegistrationController(this.runtimeRegistry, params.extensions);
|
|
3267
|
+
}
|
|
3268
|
+
get currentHandle() {
|
|
3269
|
+
return this.handle;
|
|
3270
|
+
}
|
|
3271
|
+
connectGatewayController = (gatewayController) => {
|
|
3272
|
+
this.assertNotDisposed();
|
|
3273
|
+
this.gatewayController = gatewayController;
|
|
3274
|
+
};
|
|
3275
|
+
bootstrap = async () => {
|
|
3276
|
+
this.assertNotDisposed();
|
|
3277
|
+
if (this.handle) return this.handle;
|
|
3278
|
+
const nativeRuntimeFactory = new NativeAgentRuntimeFactory({
|
|
3279
|
+
bus: this.params.bus,
|
|
3280
|
+
providerManager: this.params.providerManager,
|
|
3281
|
+
sessions: this.params.sessions,
|
|
3282
|
+
sessionRequests: this.params.sessionRequests,
|
|
3283
|
+
sessionSearch: this.params.sessionSearch,
|
|
3284
|
+
cronService: this.params.cronService,
|
|
3285
|
+
configManager: this.params.configManager,
|
|
3286
|
+
extensions: this.params.extensions,
|
|
3287
|
+
llmUsage: this.params.llmUsage,
|
|
3288
|
+
onSessionUpdated: this.params.onSessionUpdated,
|
|
3289
|
+
resolveGatewayController: () => this.gatewayController,
|
|
3290
|
+
mcpToolRegistryAdapter: this.mcpRuntimeSupport.toolRegistryAdapter,
|
|
3291
|
+
assetStore: this.assetStore,
|
|
3292
|
+
updateToolCallResult: this.updateToolCallResult
|
|
3293
|
+
});
|
|
3294
|
+
this.registerCoreRuntimes(nativeRuntimeFactory);
|
|
3295
|
+
const contextWindowPreview = new ContextCompactionPreflightService({
|
|
3296
|
+
getConfig: this.params.configManager.loadConfig,
|
|
3297
|
+
sessionManager: this.params.sessions
|
|
3298
|
+
});
|
|
3299
|
+
this.backend = new DefaultNcpAgentBackend({
|
|
3300
|
+
endpointId: "nextclaw-ui-agent",
|
|
3301
|
+
sessionStore: this.params.ncpAgentSessionStore,
|
|
3302
|
+
onSessionRunStatusChanged: this.publishSessionRunStatus,
|
|
3303
|
+
resolveSessionContextWindow: ({ messages, metadata, sessionId }) => contextWindowPreview.preview({
|
|
3304
|
+
contextWindowOwner: "nextclaw",
|
|
3305
|
+
requestMetadata: metadata,
|
|
3306
|
+
sessionId,
|
|
3307
|
+
sessionMessages: messages
|
|
3308
|
+
}),
|
|
3309
|
+
createRuntime: (runtimeParams) => {
|
|
3310
|
+
this.pluginRuntimeRegistrationController.refreshPluginRuntimeRegistrations();
|
|
3311
|
+
this.refreshConfiguredRuntimeEntries();
|
|
3312
|
+
return this.runtimeRegistry.createRuntime({
|
|
3313
|
+
...runtimeParams,
|
|
3314
|
+
resolveAssetContentPath: (assetUri) => this.assetStore.resolveContentPath(assetUri),
|
|
3315
|
+
resolveTools: nativeRuntimeFactory.resolveOpenAiToolsForRuntime
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
});
|
|
3319
|
+
this.unsubscribeAgentRuntimeRequestIngress = this.params.ingress.addHandler(AGENT_RUNTIME_SESSION_MESSAGE_INGRESS_TYPE, this.handleSessionMessageRequest);
|
|
3320
|
+
this.unsubscribeNcpEvents = this.backend.subscribe(this.publishNcpEvent);
|
|
3321
|
+
await this.backend.start();
|
|
3322
|
+
this.handle = createAgentRuntimeHandle({
|
|
3323
|
+
backend: this.backend,
|
|
3324
|
+
runtimeRegistry: this.runtimeRegistry,
|
|
3325
|
+
refreshPluginRuntimeRegistrations: this.pluginRuntimeRegistrationController.refreshPluginRuntimeRegistrations,
|
|
3326
|
+
refreshConfiguredRuntimeEntries: this.refreshConfiguredRuntimeEntries,
|
|
3327
|
+
applyMcpConfig: this.mcpRuntimeSupport.applyMcpConfig,
|
|
3328
|
+
dispose: this.dispose,
|
|
3329
|
+
assetStore: this.assetStore
|
|
3330
|
+
});
|
|
3331
|
+
return this.handle;
|
|
3332
|
+
};
|
|
3333
|
+
warmDerivedCapabilities = async () => {
|
|
3334
|
+
this.assertNotDisposed();
|
|
3335
|
+
this.warmupPromise ??= this.runDerivedCapabilityWarmup();
|
|
3336
|
+
await this.warmupPromise;
|
|
3337
|
+
};
|
|
3338
|
+
dispose = async () => {
|
|
3339
|
+
if (this.disposed) return;
|
|
3340
|
+
this.disposed = true;
|
|
3341
|
+
await this.warmupPromise?.catch(() => void 0);
|
|
3342
|
+
this.unsubscribeAgentRuntimeRequestIngress?.();
|
|
3343
|
+
this.unsubscribeAgentRuntimeRequestIngress = null;
|
|
3344
|
+
this.unsubscribeNcpEvents?.();
|
|
3345
|
+
this.unsubscribeNcpEvents = null;
|
|
3346
|
+
for (const registration of this.builtinNarpRegistrations) registration.dispose();
|
|
3347
|
+
this.pluginRuntimeRegistrationController.dispose();
|
|
3348
|
+
await this.backend?.stop();
|
|
3349
|
+
await this.mcpRuntimeSupport.dispose();
|
|
3350
|
+
};
|
|
3351
|
+
registerCoreRuntimes = (nativeRuntimeFactory) => {
|
|
3352
|
+
if (this.kernelBootstrapped) return;
|
|
3353
|
+
this.kernelBootstrapped = true;
|
|
3354
|
+
this.runtimeRegistry.register({
|
|
3355
|
+
kind: "native",
|
|
3356
|
+
label: "Native",
|
|
3357
|
+
createRuntime: nativeRuntimeFactory.create
|
|
3358
|
+
});
|
|
3359
|
+
this.builtinNarpRegistrations = new BuiltinNarpRuntimeRegistrationService(this.params.configManager.loadConfig).registerInto(this.runtimeRegistry);
|
|
3360
|
+
this.pluginRuntimeRegistrationController.refreshPluginRuntimeRegistrations();
|
|
3361
|
+
this.refreshConfiguredRuntimeEntries();
|
|
3362
|
+
};
|
|
3363
|
+
runDerivedCapabilityWarmup = async () => {
|
|
3364
|
+
const mcpWarmResults = await this.mcpRuntimeSupport.prewarmEnabledServers();
|
|
3365
|
+
for (const result of mcpWarmResults) if (!result.ok) console.warn(`[mcp] Failed to warm ${result.name}: ${result.error}`);
|
|
3366
|
+
};
|
|
3367
|
+
publishSessionRunStatus = (payload) => {
|
|
3368
|
+
this.params.eventBus.emit(eventKeys.sessionRunStatus, payload, {
|
|
3369
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3370
|
+
source: "backend"
|
|
3371
|
+
});
|
|
3372
|
+
};
|
|
3373
|
+
publishNcpEvent = (event) => {
|
|
3374
|
+
this.params.eventBus.emit(eventKeys.ncpEvent, event, {
|
|
3375
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3376
|
+
source: "backend"
|
|
3377
|
+
});
|
|
3378
|
+
this.params.handleNcpEvent(event);
|
|
3379
|
+
};
|
|
3380
|
+
handleSessionMessageRequest = async (envelope) => {
|
|
3381
|
+
const backend = this.backend;
|
|
3382
|
+
if (!backend) throw new Error("NCP backend is not ready for agent runtime requests.");
|
|
3383
|
+
const request = envelope.payload;
|
|
3384
|
+
if (!request?.requestId || !request.sessionId || !request.message) throw new Error("Invalid agent runtime session message request.");
|
|
3385
|
+
let terminalEventSeen = false;
|
|
3386
|
+
for await (const event of backend.send({
|
|
3387
|
+
sessionId: request.sessionId,
|
|
3388
|
+
message: request.message,
|
|
3389
|
+
correlationId: request.requestId
|
|
3390
|
+
})) terminalEventSeen ||= event.type === NcpEventType.MessageCompleted || event.type === NcpEventType.MessageFailed || event.type === NcpEventType.RunError;
|
|
3391
|
+
if (!terminalEventSeen) throw new Error("Session request completed without a final reply.");
|
|
3392
|
+
};
|
|
3393
|
+
updateToolCallResult = async ({ sessionId, toolCallId, result }) => {
|
|
3394
|
+
const backend = this.backend;
|
|
3395
|
+
if (!backend) throw new Error("NCP backend is not ready for tool result updates.");
|
|
3396
|
+
await backend.updateToolCallResult(sessionId, toolCallId, result);
|
|
3397
|
+
};
|
|
3398
|
+
assertNotDisposed = () => {
|
|
3399
|
+
if (this.disposed) throw new Error("Agent runtime has already been disposed.");
|
|
3400
|
+
};
|
|
3401
|
+
};
|
|
3402
|
+
//#endregion
|
|
3403
|
+
//#region src/managers/automation.manager.ts
|
|
3404
|
+
var AutomationManager = class extends CronService {
|
|
3405
|
+
constructor(options) {
|
|
3406
|
+
super(options.storePath);
|
|
3407
|
+
}
|
|
3408
|
+
};
|
|
3409
|
+
//#endregion
|
|
3410
|
+
//#region src/managers/config.manager.ts
|
|
3411
|
+
var ConfigManager = class {
|
|
3412
|
+
configPath;
|
|
3413
|
+
currentConfig;
|
|
3414
|
+
hooks = {};
|
|
3415
|
+
reloadTask = null;
|
|
3416
|
+
providerReloadTask = null;
|
|
3417
|
+
reloadTimer = null;
|
|
3418
|
+
reloadRunning = false;
|
|
3419
|
+
reloadPending = false;
|
|
3420
|
+
constructor(options) {
|
|
3421
|
+
this.options = options;
|
|
3422
|
+
this.configPath = options.configPath ?? getConfigPath();
|
|
3423
|
+
this.currentConfig = this.loadConfig();
|
|
3424
|
+
this.options.providerManager.load(this.currentConfig);
|
|
3425
|
+
this.options.channels.load({
|
|
3426
|
+
channelConfig: this.resolveChannelConfig(this.currentConfig),
|
|
3427
|
+
extensionChannels: this.resolveExtensionChannels()
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
get config() {
|
|
3431
|
+
return this.currentConfig;
|
|
3432
|
+
}
|
|
3433
|
+
loadConfig = () => resolveConfigSecrets(loadConfig(this.configPath), { configPath: this.configPath });
|
|
3434
|
+
installRuntimeHooks = (hooks) => {
|
|
3435
|
+
this.hooks = hooks;
|
|
3436
|
+
};
|
|
3437
|
+
applyLiveConfigReload = async () => {
|
|
3438
|
+
await this.applyReloadPlan(this.loadConfig());
|
|
3439
|
+
};
|
|
3440
|
+
applyReloadPlan = async (nextConfig) => {
|
|
3441
|
+
const changedPaths = diffConfigPaths(this.currentConfig, nextConfig);
|
|
3442
|
+
if (!changedPaths.length) return;
|
|
3443
|
+
this.currentConfig = nextConfig;
|
|
3444
|
+
const plan = buildReloadPlan(changedPaths);
|
|
3445
|
+
let reloadPluginsResult = void 0;
|
|
3446
|
+
if (plan.reloadPlugins) {
|
|
3447
|
+
reloadPluginsResult = await this.reloadPlugins({
|
|
3448
|
+
config: nextConfig,
|
|
3449
|
+
changedPaths
|
|
3450
|
+
});
|
|
3451
|
+
console.log("Config reload: plugins reloaded.");
|
|
3452
|
+
}
|
|
3453
|
+
if (plan.reloadMcp) {
|
|
3454
|
+
await this.reloadMcp({
|
|
3455
|
+
config: nextConfig,
|
|
3456
|
+
changedPaths
|
|
3457
|
+
});
|
|
3458
|
+
console.log("Config reload: MCP servers reloaded.");
|
|
3459
|
+
}
|
|
3460
|
+
if (plan.reloadCompanion) {
|
|
3461
|
+
await this.reloadCompanion({
|
|
3462
|
+
config: nextConfig,
|
|
3463
|
+
changedPaths
|
|
3464
|
+
});
|
|
3465
|
+
console.log("Config reload: companion setting applied.");
|
|
3466
|
+
}
|
|
3467
|
+
if (plan.restartChannels || reloadPluginsResult?.restartChannels) {
|
|
3468
|
+
await this.rebuildChannels(nextConfig, { start: true });
|
|
3469
|
+
console.log("Config reload: channels restarted.");
|
|
3470
|
+
}
|
|
3471
|
+
if (plan.reloadProviders) {
|
|
3472
|
+
await this.reloadProvider(nextConfig);
|
|
3473
|
+
console.log("Config reload: provider settings applied.");
|
|
3474
|
+
}
|
|
3475
|
+
if (plan.reloadAgent) {
|
|
3476
|
+
this.hooks.applyAgentRuntimeConfig?.(nextConfig);
|
|
3477
|
+
console.log("Config reload: agent defaults applied.");
|
|
3478
|
+
}
|
|
3479
|
+
if (plan.restartRequired.length > 0) this.hooks.onRestartRequired?.(plan.restartRequired);
|
|
3480
|
+
};
|
|
3481
|
+
scheduleReload = (reason) => {
|
|
3482
|
+
if (this.reloadTimer) clearTimeout(this.reloadTimer);
|
|
3483
|
+
this.reloadTimer = setTimeout(() => {
|
|
3484
|
+
this.runReload(reason);
|
|
3485
|
+
}, 300);
|
|
3486
|
+
};
|
|
3487
|
+
runReload = async (reason) => {
|
|
3488
|
+
if (this.reloadRunning) {
|
|
3489
|
+
this.reloadPending = true;
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
this.reloadRunning = true;
|
|
3493
|
+
if (this.reloadTimer) {
|
|
3494
|
+
clearTimeout(this.reloadTimer);
|
|
3495
|
+
this.reloadTimer = null;
|
|
3496
|
+
}
|
|
3497
|
+
try {
|
|
3498
|
+
await this.applyLiveConfigReload();
|
|
3499
|
+
} catch (error) {
|
|
3500
|
+
console.error(`Config reload failed (${reason}): ${String(error)}`);
|
|
3501
|
+
} finally {
|
|
3502
|
+
this.reloadRunning = false;
|
|
3503
|
+
if (this.reloadPending) {
|
|
3504
|
+
this.reloadPending = false;
|
|
3505
|
+
this.scheduleReload("pending");
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
reloadConfig = async (reason) => {
|
|
3510
|
+
await this.runReload(reason ?? "gateway tool");
|
|
3511
|
+
return "Config reload triggered";
|
|
3512
|
+
};
|
|
3513
|
+
rebuildChannels = async (nextConfig, options = {}) => {
|
|
3514
|
+
if (this.reloadTask) {
|
|
3515
|
+
await this.reloadTask;
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
this.reloadTask = (async () => {
|
|
3519
|
+
await this.options.channels.reload({
|
|
3520
|
+
channelConfig: this.resolveChannelConfig(nextConfig),
|
|
3521
|
+
extensionChannels: this.resolveExtensionChannels(),
|
|
3522
|
+
start: options.start ?? true
|
|
3523
|
+
});
|
|
3524
|
+
})();
|
|
3525
|
+
try {
|
|
3526
|
+
await this.reloadTask;
|
|
3527
|
+
} finally {
|
|
3528
|
+
this.reloadTask = null;
|
|
3529
|
+
}
|
|
3530
|
+
};
|
|
3531
|
+
resolveChannelConfig = (config) => this.hooks.resolveChannelConfig?.(config) ?? config;
|
|
3532
|
+
resolveExtensionChannels = () => this.hooks.getExtensionChannels?.() ?? [];
|
|
3533
|
+
reloadProvider = async (nextConfig) => {
|
|
3534
|
+
if (this.providerReloadTask) {
|
|
3535
|
+
await this.providerReloadTask;
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
this.providerReloadTask = (async () => {
|
|
3539
|
+
this.options.providerManager.load(nextConfig);
|
|
3540
|
+
})();
|
|
3541
|
+
try {
|
|
3542
|
+
await this.providerReloadTask;
|
|
3543
|
+
} finally {
|
|
3544
|
+
this.providerReloadTask = null;
|
|
3545
|
+
}
|
|
3546
|
+
};
|
|
3547
|
+
reloadPlugins = async (params) => {
|
|
3548
|
+
return await this.hooks.reloadPlugins?.(params);
|
|
3549
|
+
};
|
|
3550
|
+
reloadMcp = async (params) => {
|
|
3551
|
+
await this.hooks.reloadMcp?.(params);
|
|
3552
|
+
};
|
|
3553
|
+
reloadCompanion = async (params) => {
|
|
3554
|
+
await this.hooks.reloadCompanion?.(params);
|
|
3555
|
+
};
|
|
3556
|
+
};
|
|
3557
|
+
//#endregion
|
|
3558
|
+
//#region src/managers/extension.manager.ts
|
|
3559
|
+
function createEmptyExtensionRegistry() {
|
|
3560
|
+
return {
|
|
3561
|
+
tools: [],
|
|
3562
|
+
channels: [],
|
|
3563
|
+
ncpAgentRuntimes: [],
|
|
3564
|
+
diagnostics: []
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
3567
|
+
var ExtensionManager = class {
|
|
3568
|
+
pluginRegistry = null;
|
|
3569
|
+
extensionRegistry = createEmptyExtensionRegistry();
|
|
3570
|
+
channelBindings = [];
|
|
3571
|
+
uiMetadata = [];
|
|
3572
|
+
loadContributions = (contributions) => {
|
|
3573
|
+
this.pluginRegistry = contributions.registry;
|
|
3574
|
+
this.extensionRegistry = contributions.extensionRegistry;
|
|
3575
|
+
this.channelBindings = contributions.channelBindings ?? getPluginChannelBindings(contributions.registry);
|
|
3576
|
+
this.uiMetadata = contributions.uiMetadata ?? getPluginUiMetadataFromRegistry(contributions.registry);
|
|
3577
|
+
};
|
|
3578
|
+
loadExtensionRegistry = (extensionRegistry) => {
|
|
3579
|
+
this.extensionRegistry = extensionRegistry;
|
|
95
3580
|
};
|
|
96
|
-
|
|
97
|
-
|
|
3581
|
+
getExtensionRegistry = () => this.extensionRegistry;
|
|
3582
|
+
getChannelBindings = () => this.channelBindings;
|
|
3583
|
+
getUiMetadata = () => this.uiMetadata;
|
|
3584
|
+
toConfigView = (config) => toPluginConfigView(config, this.channelBindings);
|
|
3585
|
+
mergeConfigView = (current, nextConfigView) => mergePluginConfigView(current, nextConfigView, this.channelBindings);
|
|
3586
|
+
resolveMessageToolHints = (params) => {
|
|
3587
|
+
if (!this.pluginRegistry) return [];
|
|
3588
|
+
return resolvePluginChannelMessageToolHints({
|
|
3589
|
+
registry: this.pluginRegistry,
|
|
3590
|
+
channel: params.channel,
|
|
3591
|
+
cfg: params.config,
|
|
3592
|
+
accountId: params.accountId
|
|
3593
|
+
});
|
|
98
3594
|
};
|
|
99
|
-
|
|
100
|
-
|
|
3595
|
+
};
|
|
3596
|
+
//#endregion
|
|
3597
|
+
//#region src/configs/ncp-lifecycle-event.config.ts
|
|
3598
|
+
const agentRunStartedLifecycleEventKey = createAppEventKey("agent.run.started");
|
|
3599
|
+
const agentRunFinishedLifecycleEventKey = createAppEventKey("agent.run.finished");
|
|
3600
|
+
const agentMessageSentLifecycleEventKey = createAppEventKey("agent.message.sent");
|
|
3601
|
+
const agentSessionUpdatedLifecycleEventKey = createAppEventKey("agent.session.updated");
|
|
3602
|
+
//#endregion
|
|
3603
|
+
//#region src/configs/learning-loop.config.ts
|
|
3604
|
+
const LEARNING_LOOP_DISABLED_METADATA_KEY = "learning_loop_disabled";
|
|
3605
|
+
const LEARNING_LOOP_LAST_TOOL_CALL_COUNT_METADATA_KEY = "learning_loop_last_tool_call_count";
|
|
3606
|
+
const LEARNING_LOOP_LAST_REQUESTED_AT_METADATA_KEY = "learning_loop_last_requested_at";
|
|
3607
|
+
const LEARNING_LOOP_LAST_REVIEW_SESSION_ID_METADATA_KEY = "learning_loop_last_review_session_id";
|
|
3608
|
+
const LEARNING_LOOP_SOURCE_SESSION_ID_METADATA_KEY = "learning_loop_source_session_id";
|
|
3609
|
+
const LEARNING_LOOP_REQUESTED_SKILLS = ["skill-creator"];
|
|
3610
|
+
const DEFAULT_LEARNING_LOOP_TOOL_CALL_THRESHOLD = 15;
|
|
3611
|
+
function readLearningLoopRuntimeConfig(config) {
|
|
3612
|
+
return {
|
|
3613
|
+
enabled: config.agents.learningLoop.enabled,
|
|
3614
|
+
toolCallThreshold: config.agents.learningLoop.toolCallThreshold ?? 15
|
|
3615
|
+
};
|
|
3616
|
+
}
|
|
3617
|
+
//#endregion
|
|
3618
|
+
//#region src/utils/learning-loop-prompt.utils.ts
|
|
3619
|
+
function pluralizeToolCalls(value) {
|
|
3620
|
+
return value === 1 ? "1 tool call" : `${value} tool calls`;
|
|
3621
|
+
}
|
|
3622
|
+
function buildLearningLoopTask(params) {
|
|
3623
|
+
return [
|
|
3624
|
+
"Run a background learning-loop review for the parent session.",
|
|
3625
|
+
`Parent session id: ${params.sessionId}.`,
|
|
3626
|
+
`There have been ${pluralizeToolCalls(params.toolCallsSinceReview)} since the last learning-loop review (${params.currentToolCallCount} total so far).`,
|
|
3627
|
+
"",
|
|
3628
|
+
"Your job is to extract only the reusable lesson.",
|
|
3629
|
+
"- Review the parent session history and, if helpful, use session_search to compare similar historical sessions.",
|
|
3630
|
+
"- Decide exactly one outcome: no_skill_change, patch_existing_skill, or create_new_skill.",
|
|
3631
|
+
"- Prefer patching an existing skill when the lesson extends an existing workflow.",
|
|
3632
|
+
"- Only create a new skill when the lesson is clearly reusable, repeatable, and likely to recur.",
|
|
3633
|
+
"- Do not create or patch a skill for one-off quirks, noisy transcripts, or narrow local accidents.",
|
|
3634
|
+
"",
|
|
3635
|
+
"If you decide to patch or create a skill:",
|
|
3636
|
+
"- Use the available file tools.",
|
|
3637
|
+
"- Keep the change minimal and maintainable.",
|
|
3638
|
+
"- Write the skill into the current workspace/project skills directory under skills/<slug>/SKILL.md.",
|
|
3639
|
+
"- Follow the built-in skill-creator guidance when it helps.",
|
|
3640
|
+
"",
|
|
3641
|
+
"If nothing reusable should be saved, stop without writing files."
|
|
3642
|
+
].join("\n");
|
|
3643
|
+
}
|
|
3644
|
+
//#endregion
|
|
3645
|
+
//#region src/managers/learning-loop.manager.ts
|
|
3646
|
+
function countToolCallsFromMessage(message) {
|
|
3647
|
+
if (Array.isArray(message.tool_calls)) return message.tool_calls.filter((toolCall) => Boolean(toolCall) && typeof toolCall === "object" && !Array.isArray(toolCall)).length;
|
|
3648
|
+
if (!Array.isArray(message.ncp_parts)) return 0;
|
|
3649
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
3650
|
+
let anonymousCount = 0;
|
|
3651
|
+
for (const part of message.ncp_parts) {
|
|
3652
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) continue;
|
|
3653
|
+
const candidate = part;
|
|
3654
|
+
if (candidate.type !== "tool-invocation") continue;
|
|
3655
|
+
if (typeof candidate.toolCallId === "string" && candidate.toolCallId.trim()) {
|
|
3656
|
+
seenIds.add(candidate.toolCallId.trim());
|
|
3657
|
+
continue;
|
|
3658
|
+
}
|
|
3659
|
+
anonymousCount += 1;
|
|
3660
|
+
}
|
|
3661
|
+
return seenIds.size + anonymousCount;
|
|
3662
|
+
}
|
|
3663
|
+
function countSessionToolCalls(session) {
|
|
3664
|
+
return session.messages.reduce((count, message) => count + countToolCallsFromMessage(message), 0);
|
|
3665
|
+
}
|
|
3666
|
+
function readSessionLabel(metadata) {
|
|
3667
|
+
const label = metadata.label;
|
|
3668
|
+
return typeof label === "string" && label.trim().length > 0 ? label.trim() : void 0;
|
|
3669
|
+
}
|
|
3670
|
+
function isLearningLoopDisabled(metadata) {
|
|
3671
|
+
return metadata[LEARNING_LOOP_DISABLED_METADATA_KEY] === true;
|
|
3672
|
+
}
|
|
3673
|
+
function readLearningLoopLastToolCallCount(metadata) {
|
|
3674
|
+
const value = metadata[LEARNING_LOOP_LAST_TOOL_CALL_COUNT_METADATA_KEY];
|
|
3675
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
3676
|
+
}
|
|
3677
|
+
var LearningLoopManager = class {
|
|
3678
|
+
sessionStore;
|
|
3679
|
+
inFlightSessionIds = /* @__PURE__ */ new Set();
|
|
3680
|
+
unsubscribe = null;
|
|
3681
|
+
constructor(options) {
|
|
3682
|
+
this.options = options;
|
|
3683
|
+
this.sessionStore = options.sessionManager;
|
|
3684
|
+
}
|
|
3685
|
+
start = () => {
|
|
3686
|
+
if (this.unsubscribe) return;
|
|
3687
|
+
this.unsubscribe = this.options.eventBus.on(agentRunFinishedLifecycleEventKey, this.handleRunFinished);
|
|
3688
|
+
};
|
|
3689
|
+
dispose = () => {
|
|
3690
|
+
this.unsubscribe?.();
|
|
3691
|
+
this.unsubscribe = null;
|
|
101
3692
|
};
|
|
102
|
-
|
|
103
|
-
|
|
3693
|
+
handleRunFinished = (event) => {
|
|
3694
|
+
this.handleRunFinishedInBackground(event).catch((error) => {
|
|
3695
|
+
console.warn(`[learning-loop] Failed for ${event.sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3696
|
+
});
|
|
104
3697
|
};
|
|
105
|
-
|
|
106
|
-
|
|
3698
|
+
handleRunFinishedInBackground = async (event) => {
|
|
3699
|
+
if (event.isChildSession || this.inFlightSessionIds.has(event.sessionId)) return;
|
|
3700
|
+
const session = this.sessionStore.getIfExists(event.sessionId);
|
|
3701
|
+
if (!session) return;
|
|
3702
|
+
const runtimeConfig = this.readRuntimeConfig();
|
|
3703
|
+
if (!runtimeConfig.enabled || isLearningLoopDisabled(session.metadata)) return;
|
|
3704
|
+
const totalToolCalls = countSessionToolCalls(session);
|
|
3705
|
+
const toolCallsSinceReview = totalToolCalls - readLearningLoopLastToolCallCount(session.metadata);
|
|
3706
|
+
if (toolCallsSinceReview < runtimeConfig.toolCallThreshold) return;
|
|
3707
|
+
this.inFlightSessionIds.add(event.sessionId);
|
|
3708
|
+
try {
|
|
3709
|
+
const reviewSession = await this.options.sessionRequester.spawnSessionAndRequest({
|
|
3710
|
+
sourceSessionId: event.sessionId,
|
|
3711
|
+
updateToolCallResult: async () => void 0,
|
|
3712
|
+
sourceSessionMetadata: session.metadata,
|
|
3713
|
+
metadataOverrides: {
|
|
3714
|
+
requested_skills: LEARNING_LOOP_REQUESTED_SKILLS,
|
|
3715
|
+
[LEARNING_LOOP_DISABLED_METADATA_KEY]: true,
|
|
3716
|
+
[LEARNING_LOOP_SOURCE_SESSION_ID_METADATA_KEY]: event.sessionId
|
|
3717
|
+
},
|
|
3718
|
+
parentSessionId: event.sessionId,
|
|
3719
|
+
notify: "none",
|
|
3720
|
+
title: this.buildReviewTitle(session.metadata),
|
|
3721
|
+
task: buildLearningLoopTask({
|
|
3722
|
+
sessionId: event.sessionId,
|
|
3723
|
+
toolCallsSinceReview,
|
|
3724
|
+
currentToolCallCount: totalToolCalls
|
|
3725
|
+
})
|
|
3726
|
+
});
|
|
3727
|
+
session.metadata = {
|
|
3728
|
+
...session.metadata,
|
|
3729
|
+
[LEARNING_LOOP_LAST_TOOL_CALL_COUNT_METADATA_KEY]: totalToolCalls,
|
|
3730
|
+
[LEARNING_LOOP_LAST_REQUESTED_AT_METADATA_KEY]: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3731
|
+
[LEARNING_LOOP_LAST_REVIEW_SESSION_ID_METADATA_KEY]: reviewSession.sessionId
|
|
3732
|
+
};
|
|
3733
|
+
this.sessionStore.save(session);
|
|
3734
|
+
} finally {
|
|
3735
|
+
this.inFlightSessionIds.delete(event.sessionId);
|
|
3736
|
+
}
|
|
107
3737
|
};
|
|
108
|
-
|
|
109
|
-
|
|
3738
|
+
readRuntimeConfig = () => {
|
|
3739
|
+
return this.options.resolveLearningLoopConfig?.() ?? {
|
|
3740
|
+
enabled: true,
|
|
3741
|
+
toolCallThreshold: 15
|
|
3742
|
+
};
|
|
110
3743
|
};
|
|
111
|
-
|
|
112
|
-
|
|
3744
|
+
buildReviewTitle = (metadata) => {
|
|
3745
|
+
const label = readSessionLabel(metadata);
|
|
3746
|
+
return label ? `Learning loop: ${label}` : "Learning loop";
|
|
113
3747
|
};
|
|
114
3748
|
};
|
|
115
3749
|
//#endregion
|
|
116
|
-
//#region src/managers/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
3750
|
+
//#region src/managers/llm-provider.manager.ts
|
|
3751
|
+
const normalizedModel = (value) => {
|
|
3752
|
+
if (typeof value !== "string") return null;
|
|
3753
|
+
const trimmed = value.trim();
|
|
3754
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
3755
|
+
};
|
|
3756
|
+
const headersFingerprint = (headers) => {
|
|
3757
|
+
if (!headers) return "";
|
|
3758
|
+
const sortedEntries = Object.entries(headers).sort(([left], [right]) => left.localeCompare(right));
|
|
3759
|
+
return JSON.stringify(sortedEntries);
|
|
3760
|
+
};
|
|
3761
|
+
var MissingKernelProvider = class extends LLMProvider {
|
|
3762
|
+
constructor(defaultModel) {
|
|
3763
|
+
super(null, null);
|
|
3764
|
+
this.defaultModel = defaultModel;
|
|
3765
|
+
}
|
|
3766
|
+
setDefaultModel = (model) => {
|
|
3767
|
+
this.defaultModel = model;
|
|
3768
|
+
};
|
|
3769
|
+
getDefaultModel = () => {
|
|
3770
|
+
return this.defaultModel;
|
|
3771
|
+
};
|
|
3772
|
+
chat = async (_params) => {
|
|
3773
|
+
throw new Error("No API key configured yet. Configure provider credentials in UI and retry.");
|
|
3774
|
+
};
|
|
3775
|
+
};
|
|
3776
|
+
var LlmProviderManager = class {
|
|
3777
|
+
providerRegistry;
|
|
3778
|
+
providerPool = /* @__PURE__ */ new Map();
|
|
3779
|
+
missingProvider = new MissingKernelProvider("gpt-4o");
|
|
3780
|
+
config = null;
|
|
3781
|
+
constructor() {
|
|
3782
|
+
this.providerRegistry = new ProviderRegistry(BUILTIN_PROVIDER_PLUGINS);
|
|
3783
|
+
}
|
|
3784
|
+
load = (config) => {
|
|
3785
|
+
this.config = config;
|
|
3786
|
+
this.providerPool.clear();
|
|
3787
|
+
this.missingProvider.setDefaultModel(config.agents.defaults.model);
|
|
120
3788
|
};
|
|
121
|
-
|
|
122
|
-
|
|
3789
|
+
get = (model) => {
|
|
3790
|
+
const route = this.resolveRoute(model);
|
|
3791
|
+
if (!route) return this.missingProvider;
|
|
3792
|
+
return this.getOrCreateProvider(route);
|
|
123
3793
|
};
|
|
124
|
-
|
|
125
|
-
|
|
3794
|
+
chat = async (params) => {
|
|
3795
|
+
return this.get(params.model ?? null).chat(params);
|
|
126
3796
|
};
|
|
127
|
-
|
|
128
|
-
|
|
3797
|
+
chatStream = async function* (params) {
|
|
3798
|
+
for await (const event of this.get(params.model ?? null).chatStream(params)) yield event;
|
|
129
3799
|
};
|
|
130
|
-
|
|
131
|
-
|
|
3800
|
+
testConnection = async (input) => {
|
|
3801
|
+
await this.createProvider({
|
|
3802
|
+
providerName: input.providerName,
|
|
3803
|
+
provider: {
|
|
3804
|
+
enabled: true,
|
|
3805
|
+
displayName: "",
|
|
3806
|
+
apiKey: input.apiKey ?? "",
|
|
3807
|
+
apiBase: input.apiBase ?? null,
|
|
3808
|
+
extraHeaders: input.extraHeaders ?? null,
|
|
3809
|
+
wireApi: input.wireApi ?? "auto",
|
|
3810
|
+
models: [],
|
|
3811
|
+
modelThinking: {}
|
|
3812
|
+
},
|
|
3813
|
+
apiBase: input.apiBase ?? null,
|
|
3814
|
+
model: input.defaultModel
|
|
3815
|
+
}).chat({
|
|
3816
|
+
messages: input.messages,
|
|
3817
|
+
model: input.defaultModel,
|
|
3818
|
+
maxTokens: input.maxTokens,
|
|
3819
|
+
signal: input.signal
|
|
3820
|
+
});
|
|
132
3821
|
};
|
|
133
|
-
|
|
134
|
-
|
|
3822
|
+
resolveRoute = (model) => {
|
|
3823
|
+
if (!this.config) return null;
|
|
3824
|
+
const effectiveModel = normalizedModel(model) ?? this.config.agents.defaults.model;
|
|
3825
|
+
const { provider, name } = this.resolveProvider(effectiveModel);
|
|
3826
|
+
const providerSpec = name ? this.providerRegistry.findProviderByName(name) : void 0;
|
|
3827
|
+
return {
|
|
3828
|
+
model: effectiveModel,
|
|
3829
|
+
providerName: name,
|
|
3830
|
+
provider,
|
|
3831
|
+
apiBase: provider?.apiBase ?? providerSpec?.defaultApiBase ?? null
|
|
3832
|
+
};
|
|
3833
|
+
};
|
|
3834
|
+
resolveProvider = (model) => {
|
|
3835
|
+
const providers = this.config?.providers;
|
|
3836
|
+
if (!providers) return {
|
|
3837
|
+
provider: null,
|
|
3838
|
+
name: null
|
|
3839
|
+
};
|
|
3840
|
+
const specs = this.providerRegistry.listProviderSpecs();
|
|
3841
|
+
const modelLower = model.toLowerCase();
|
|
3842
|
+
const modelPrefix = modelLower.includes("/") ? modelLower.slice(0, modelLower.indexOf("/")) : "";
|
|
3843
|
+
if (modelPrefix) {
|
|
3844
|
+
const prefixed = this.matchPrefixedProvider(providers, modelPrefix);
|
|
3845
|
+
if (prefixed) return prefixed;
|
|
3846
|
+
}
|
|
3847
|
+
const keywordMatches = specs.map((spec) => ({
|
|
3848
|
+
name: spec.name,
|
|
3849
|
+
provider: providers[spec.name],
|
|
3850
|
+
spec
|
|
3851
|
+
})).filter((entry) => entry.provider?.enabled !== false && Boolean(entry.provider?.apiKey)).filter((entry) => entry.spec.keywords.some((keyword) => modelLower.includes(keyword)));
|
|
3852
|
+
if (keywordMatches.length === 1) {
|
|
3853
|
+
const match = keywordMatches[0];
|
|
3854
|
+
return {
|
|
3855
|
+
provider: match.provider ?? null,
|
|
3856
|
+
name: match.name
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
3859
|
+
if (keywordMatches.length > 1) return {
|
|
3860
|
+
provider: null,
|
|
3861
|
+
name: null
|
|
3862
|
+
};
|
|
3863
|
+
const builtinNames = new Set(specs.map((spec) => spec.name));
|
|
3864
|
+
const enabledProviders = Object.entries(providers).filter(([, provider]) => provider.enabled !== false && Boolean(provider.apiKey));
|
|
3865
|
+
const enabledBuiltin = enabledProviders.filter(([name]) => builtinNames.has(name));
|
|
3866
|
+
if (enabledBuiltin.length === 1) {
|
|
3867
|
+
const [name, provider] = enabledBuiltin[0];
|
|
3868
|
+
return {
|
|
3869
|
+
provider,
|
|
3870
|
+
name
|
|
3871
|
+
};
|
|
3872
|
+
}
|
|
3873
|
+
const enabledCustom = enabledProviders.filter(([name]) => !builtinNames.has(name));
|
|
3874
|
+
if (enabledCustom.length === 1) {
|
|
3875
|
+
const [name, provider] = enabledCustom[0];
|
|
3876
|
+
return {
|
|
3877
|
+
provider,
|
|
3878
|
+
name
|
|
3879
|
+
};
|
|
3880
|
+
}
|
|
3881
|
+
return {
|
|
3882
|
+
provider: null,
|
|
3883
|
+
name: null
|
|
3884
|
+
};
|
|
3885
|
+
};
|
|
3886
|
+
matchPrefixedProvider = (providers, modelPrefix) => {
|
|
3887
|
+
for (const spec of this.providerRegistry.listProviderSpecs()) {
|
|
3888
|
+
const provider = providers[spec.name];
|
|
3889
|
+
const aliases = [spec.name, spec.modelPrefix ?? ""].map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
3890
|
+
if (provider && aliases.includes(modelPrefix)) return provider.enabled !== false ? {
|
|
3891
|
+
provider,
|
|
3892
|
+
name: spec.name
|
|
3893
|
+
} : {
|
|
3894
|
+
provider: null,
|
|
3895
|
+
name: null
|
|
3896
|
+
};
|
|
3897
|
+
}
|
|
3898
|
+
const customProvider = Object.entries(providers).find(([name]) => name.toLowerCase() === modelPrefix);
|
|
3899
|
+
if (!customProvider) return null;
|
|
3900
|
+
const [name, provider] = customProvider;
|
|
3901
|
+
return provider.enabled !== false ? {
|
|
3902
|
+
provider,
|
|
3903
|
+
name
|
|
3904
|
+
} : {
|
|
3905
|
+
provider: null,
|
|
3906
|
+
name: null
|
|
3907
|
+
};
|
|
135
3908
|
};
|
|
136
|
-
|
|
137
|
-
|
|
3909
|
+
getOrCreateProvider = (route) => {
|
|
3910
|
+
if (!route.provider?.apiKey && !route.model.startsWith("bedrock/")) return this.missingProvider;
|
|
3911
|
+
const cacheKey = this.buildCacheKey(route);
|
|
3912
|
+
const cached = this.providerPool.get(cacheKey);
|
|
3913
|
+
if (cached) return cached;
|
|
3914
|
+
const created = this.createProvider(route);
|
|
3915
|
+
this.providerPool.set(cacheKey, created);
|
|
3916
|
+
return created;
|
|
138
3917
|
};
|
|
139
|
-
|
|
140
|
-
|
|
3918
|
+
createProvider = (route) => {
|
|
3919
|
+
return new LiteLLMProvider({
|
|
3920
|
+
apiKey: route.provider?.apiKey ?? null,
|
|
3921
|
+
apiBase: route.apiBase,
|
|
3922
|
+
defaultModel: route.model,
|
|
3923
|
+
extraHeaders: route.provider?.extraHeaders ?? null,
|
|
3924
|
+
providerName: route.providerName,
|
|
3925
|
+
providerRegistry: this.providerRegistry,
|
|
3926
|
+
wireApi: route.provider?.wireApi ?? null
|
|
3927
|
+
});
|
|
3928
|
+
};
|
|
3929
|
+
buildCacheKey = (route) => {
|
|
3930
|
+
const routeProvider = route.provider;
|
|
3931
|
+
return [
|
|
3932
|
+
route.providerName ?? "",
|
|
3933
|
+
routeProvider?.apiKey ?? "",
|
|
3934
|
+
route.apiBase ?? "",
|
|
3935
|
+
routeProvider?.wireApi ?? "",
|
|
3936
|
+
headersFingerprint(routeProvider?.extraHeaders ?? null)
|
|
3937
|
+
].join("||");
|
|
141
3938
|
};
|
|
142
3939
|
};
|
|
143
3940
|
//#endregion
|
|
@@ -166,37 +3963,6 @@ var SkillManager = class {
|
|
|
166
3963
|
};
|
|
167
3964
|
};
|
|
168
3965
|
//#endregion
|
|
169
|
-
//#region src/managers/task.manager.ts
|
|
170
|
-
var TaskManager = class {
|
|
171
|
-
listTasks = () => {
|
|
172
|
-
throw new Error("TaskManager.listTasks is not implemented.");
|
|
173
|
-
};
|
|
174
|
-
getTask = (taskId) => {
|
|
175
|
-
throw new Error("TaskManager.getTask is not implemented.");
|
|
176
|
-
};
|
|
177
|
-
requireTask = (taskId) => {
|
|
178
|
-
throw new Error("TaskManager.requireTask is not implemented.");
|
|
179
|
-
};
|
|
180
|
-
createTask = (input) => {
|
|
181
|
-
throw new Error("TaskManager.createTask is not implemented.");
|
|
182
|
-
};
|
|
183
|
-
saveTask = (task) => {
|
|
184
|
-
throw new Error("TaskManager.saveTask is not implemented.");
|
|
185
|
-
};
|
|
186
|
-
updateTaskStatus = (taskId, status) => {
|
|
187
|
-
throw new Error("TaskManager.updateTaskStatus is not implemented.");
|
|
188
|
-
};
|
|
189
|
-
completeTask = (taskId, output) => {
|
|
190
|
-
throw new Error("TaskManager.completeTask is not implemented.");
|
|
191
|
-
};
|
|
192
|
-
failTask = (taskId, error) => {
|
|
193
|
-
throw new Error("TaskManager.failTask is not implemented.");
|
|
194
|
-
};
|
|
195
|
-
cancelTask = (taskId) => {
|
|
196
|
-
throw new Error("TaskManager.cancelTask is not implemented.");
|
|
197
|
-
};
|
|
198
|
-
};
|
|
199
|
-
//#endregion
|
|
200
3966
|
//#region src/managers/tool.manager.ts
|
|
201
3967
|
var ToolManager = class {
|
|
202
3968
|
listTools = () => {
|
|
@@ -222,7 +3988,312 @@ var ToolManager = class {
|
|
|
222
3988
|
};
|
|
223
3989
|
};
|
|
224
3990
|
//#endregion
|
|
3991
|
+
//#region src/services/ncp-lifecycle-event-bridge.service.ts
|
|
3992
|
+
function readSessionType(metadata) {
|
|
3993
|
+
const sessionType = metadata?.session_type;
|
|
3994
|
+
return typeof sessionType === "string" && sessionType.trim().length > 0 ? sessionType.trim() : void 0;
|
|
3995
|
+
}
|
|
3996
|
+
var NcpLifecycleEventBridge = class {
|
|
3997
|
+
constructor(sessionManager, eventBus) {
|
|
3998
|
+
this.sessionManager = sessionManager;
|
|
3999
|
+
this.eventBus = eventBus;
|
|
4000
|
+
}
|
|
4001
|
+
publishSessionUpdated = (sessionId) => {
|
|
4002
|
+
const context = this.buildSessionContext(sessionId);
|
|
4003
|
+
if (!context) return;
|
|
4004
|
+
const payload = { ...context };
|
|
4005
|
+
this.eventBus.emit(agentSessionUpdatedLifecycleEventKey, payload);
|
|
4006
|
+
};
|
|
4007
|
+
handleEndpointEvent = (event) => {
|
|
4008
|
+
switch (event.type) {
|
|
4009
|
+
case NcpEventType.RunStarted:
|
|
4010
|
+
this.publishRunStarted(event);
|
|
4011
|
+
return;
|
|
4012
|
+
case NcpEventType.RunFinished:
|
|
4013
|
+
this.publishRunFinished(event);
|
|
4014
|
+
return;
|
|
4015
|
+
case NcpEventType.MessageSent:
|
|
4016
|
+
this.publishMessageSent(event);
|
|
4017
|
+
return;
|
|
4018
|
+
default: return;
|
|
4019
|
+
}
|
|
4020
|
+
};
|
|
4021
|
+
publishRunStarted = (event) => {
|
|
4022
|
+
const sessionId = event.payload.sessionId?.trim();
|
|
4023
|
+
if (!sessionId) return;
|
|
4024
|
+
const context = this.buildSessionContext(sessionId);
|
|
4025
|
+
if (!context) return;
|
|
4026
|
+
const payload = {
|
|
4027
|
+
...context,
|
|
4028
|
+
...event.payload.runId ? { runId: event.payload.runId } : {},
|
|
4029
|
+
...event.payload.messageId ? { messageId: event.payload.messageId } : {}
|
|
4030
|
+
};
|
|
4031
|
+
this.eventBus.emit(agentRunStartedLifecycleEventKey, payload);
|
|
4032
|
+
};
|
|
4033
|
+
publishRunFinished = (event) => {
|
|
4034
|
+
const sessionId = event.payload.sessionId?.trim();
|
|
4035
|
+
if (!sessionId) return;
|
|
4036
|
+
const context = this.buildSessionContext(sessionId);
|
|
4037
|
+
if (!context) return;
|
|
4038
|
+
const payload = {
|
|
4039
|
+
...context,
|
|
4040
|
+
...event.payload.runId ? { runId: event.payload.runId } : {},
|
|
4041
|
+
...event.payload.messageId ? { messageId: event.payload.messageId } : {}
|
|
4042
|
+
};
|
|
4043
|
+
this.eventBus.emit(agentRunFinishedLifecycleEventKey, payload);
|
|
4044
|
+
};
|
|
4045
|
+
publishMessageSent = (event) => {
|
|
4046
|
+
const sessionId = event.payload.sessionId.trim();
|
|
4047
|
+
const context = this.buildSessionContext(sessionId);
|
|
4048
|
+
if (!context) return;
|
|
4049
|
+
const payload = {
|
|
4050
|
+
...context,
|
|
4051
|
+
messageId: event.payload.message.id,
|
|
4052
|
+
role: event.payload.message.role
|
|
4053
|
+
};
|
|
4054
|
+
this.eventBus.emit(agentMessageSentLifecycleEventKey, payload);
|
|
4055
|
+
};
|
|
4056
|
+
buildSessionContext = (sessionId) => {
|
|
4057
|
+
const metadata = this.sessionManager.getIfExists(sessionId)?.metadata;
|
|
4058
|
+
const sessionType = readSessionType(metadata);
|
|
4059
|
+
const parentSessionId = metadata ? readParentSessionId(metadata) : void 0;
|
|
4060
|
+
return {
|
|
4061
|
+
sessionId,
|
|
4062
|
+
...sessionType ? { sessionType } : {},
|
|
4063
|
+
...parentSessionId ? { parentSessionId } : {},
|
|
4064
|
+
isChildSession: Boolean(parentSessionId),
|
|
4065
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4066
|
+
};
|
|
4067
|
+
};
|
|
4068
|
+
};
|
|
4069
|
+
//#endregion
|
|
4070
|
+
//#region src/utils/ncp-session-summary.utils.ts
|
|
4071
|
+
function createNcpSessionSummary(params) {
|
|
4072
|
+
const { sessionId, agentId, messages, updatedAt, status, metadata, contextWindow } = params;
|
|
4073
|
+
return {
|
|
4074
|
+
sessionId,
|
|
4075
|
+
...agentId ? { agentId } : {},
|
|
4076
|
+
messageCount: messages.length,
|
|
4077
|
+
updatedAt,
|
|
4078
|
+
...messages.length > 0 ? { lastMessageAt: messages[messages.length - 1]?.timestamp ?? updatedAt } : {},
|
|
4079
|
+
status,
|
|
4080
|
+
...metadata ? { metadata: structuredClone(metadata) } : {},
|
|
4081
|
+
...contextWindow ? { contextWindow: structuredClone(contextWindow) } : {}
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
//#endregion
|
|
4085
|
+
//#region src/services/ncp-session-api.service.ts
|
|
4086
|
+
function applyLimit(items, limit) {
|
|
4087
|
+
if (!Number.isFinite(limit) || typeof limit !== "number" || limit <= 0) return items;
|
|
4088
|
+
return items.slice(0, Math.trunc(limit));
|
|
4089
|
+
}
|
|
4090
|
+
function formatBackgroundTaskError(error) {
|
|
4091
|
+
if (error instanceof Error) return error.stack ?? error.message;
|
|
4092
|
+
return String(error);
|
|
4093
|
+
}
|
|
4094
|
+
function buildUpdatedMetadata(params) {
|
|
4095
|
+
const { existingMetadata, patch } = params;
|
|
4096
|
+
if (patch.metadata === null) return {};
|
|
4097
|
+
if (patch.metadata) return structuredClone(patch.metadata);
|
|
4098
|
+
return structuredClone(existingMetadata ?? {});
|
|
4099
|
+
}
|
|
4100
|
+
function normalizeSessionId(sessionId) {
|
|
4101
|
+
return sessionId.trim();
|
|
4102
|
+
}
|
|
4103
|
+
var NcpSessionApiService = class {
|
|
4104
|
+
unsubscribeSessionUpdated = null;
|
|
4105
|
+
contextWindowPreview;
|
|
4106
|
+
constructor(options) {
|
|
4107
|
+
this.options = options;
|
|
4108
|
+
this.contextWindowPreview = new ContextCompactionPreflightService({
|
|
4109
|
+
getConfig: options.getConfig,
|
|
4110
|
+
sessionManager: options.sessionManager
|
|
4111
|
+
});
|
|
4112
|
+
}
|
|
4113
|
+
start = () => {
|
|
4114
|
+
if (this.unsubscribeSessionUpdated) return;
|
|
4115
|
+
this.unsubscribeSessionUpdated = this.options.eventBus.on(eventKeys.sessionUpdated, ({ sessionKey }) => {
|
|
4116
|
+
this.publishSessionChange(sessionKey).catch((error) => {
|
|
4117
|
+
console.error(`[session-realtime] failed to publish session change for ${sessionKey}: ${formatBackgroundTaskError(error)}`);
|
|
4118
|
+
});
|
|
4119
|
+
});
|
|
4120
|
+
};
|
|
4121
|
+
dispose = () => {
|
|
4122
|
+
this.unsubscribeSessionUpdated?.();
|
|
4123
|
+
this.unsubscribeSessionUpdated = null;
|
|
4124
|
+
};
|
|
4125
|
+
publishSessionChange = async (sessionKey) => {
|
|
4126
|
+
const normalizedSessionKey = normalizeSessionId(sessionKey);
|
|
4127
|
+
if (!normalizedSessionKey) return;
|
|
4128
|
+
const summary = await this.getSession(normalizedSessionKey);
|
|
4129
|
+
if (summary) {
|
|
4130
|
+
this.options.eventBus.emit(eventKeys.sessionSummaryUpsert, { summary });
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
this.options.eventBus.emit(eventKeys.sessionSummaryDelete, { sessionKey: normalizedSessionKey });
|
|
4134
|
+
};
|
|
4135
|
+
listSessions = async (options) => {
|
|
4136
|
+
const summaries = [];
|
|
4137
|
+
for (const record of this.options.sessionManager.listSessions()) {
|
|
4138
|
+
const sessionId = normalizeSessionId(record.key);
|
|
4139
|
+
if (!sessionId) continue;
|
|
4140
|
+
const session = this.options.sessionManager.getIfExists(sessionId);
|
|
4141
|
+
if (!session) continue;
|
|
4142
|
+
summaries.push(createNcpSessionSummary({
|
|
4143
|
+
sessionId,
|
|
4144
|
+
agentId: session.agentId,
|
|
4145
|
+
messages: toNcpMessages(sessionId, session.messages),
|
|
4146
|
+
updatedAt: session.updatedAt.toISOString(),
|
|
4147
|
+
status: "idle",
|
|
4148
|
+
metadata: session.metadata
|
|
4149
|
+
}));
|
|
4150
|
+
}
|
|
4151
|
+
summaries.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
4152
|
+
return applyLimit(summaries, options?.limit);
|
|
4153
|
+
};
|
|
4154
|
+
listSessionMessages = async (sessionId, options) => {
|
|
4155
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
4156
|
+
const session = normalizedSessionId ? this.options.sessionManager.getIfExists(normalizedSessionId) : null;
|
|
4157
|
+
if (!session) return [];
|
|
4158
|
+
return applyLimit(toNcpMessages(normalizedSessionId, session.messages).map((message) => structuredClone(message)), options?.limit);
|
|
4159
|
+
};
|
|
4160
|
+
getSession = async (sessionId) => {
|
|
4161
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
4162
|
+
const session = normalizedSessionId ? this.options.sessionManager.getIfExists(normalizedSessionId) : null;
|
|
4163
|
+
if (!session) return null;
|
|
4164
|
+
const messages = toNcpMessages(normalizedSessionId, session.messages);
|
|
4165
|
+
return createNcpSessionSummary({
|
|
4166
|
+
sessionId: normalizedSessionId,
|
|
4167
|
+
agentId: session.agentId,
|
|
4168
|
+
messages,
|
|
4169
|
+
updatedAt: session.updatedAt.toISOString(),
|
|
4170
|
+
status: "idle",
|
|
4171
|
+
metadata: session.metadata,
|
|
4172
|
+
contextWindow: this.contextWindowPreview.preview({
|
|
4173
|
+
contextWindowOwner: "nextclaw",
|
|
4174
|
+
requestMetadata: session.metadata,
|
|
4175
|
+
sessionId: normalizedSessionId,
|
|
4176
|
+
sessionMessages: messages
|
|
4177
|
+
})
|
|
4178
|
+
});
|
|
4179
|
+
};
|
|
4180
|
+
updateSession = async (sessionId, patch) => {
|
|
4181
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
4182
|
+
const session = normalizedSessionId ? this.options.sessionManager.getIfExists(normalizedSessionId) : null;
|
|
4183
|
+
if (!session) return null;
|
|
4184
|
+
session.metadata = buildUpdatedMetadata({
|
|
4185
|
+
existingMetadata: session.metadata,
|
|
4186
|
+
patch
|
|
4187
|
+
});
|
|
4188
|
+
session.updatedAt = /* @__PURE__ */ new Date();
|
|
4189
|
+
this.options.sessionManager.save(session);
|
|
4190
|
+
await this.publishSessionChange(normalizedSessionId);
|
|
4191
|
+
return await this.getSession(normalizedSessionId);
|
|
4192
|
+
};
|
|
4193
|
+
deleteSession = async (sessionId) => {
|
|
4194
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
4195
|
+
if (!normalizedSessionId) return;
|
|
4196
|
+
this.options.sessionManager.delete(normalizedSessionId);
|
|
4197
|
+
await this.publishSessionChange(normalizedSessionId);
|
|
4198
|
+
};
|
|
4199
|
+
};
|
|
4200
|
+
//#endregion
|
|
4201
|
+
//#region src/utils/ncp-session-metadata.utils.ts
|
|
4202
|
+
function resolvePersistedSessionMetadata(params) {
|
|
4203
|
+
const { currentMetadata, preserveExistingMetadata, sessionRecord } = params;
|
|
4204
|
+
const messageMetadata = extractMessageMetadata(sessionRecord.messages);
|
|
4205
|
+
return mergeSessionMetadata(preserveExistingMetadata ? mergeSessionMetadata(currentMetadata, messageMetadata) : mergeSessionMetadata({}, messageMetadata), cloneMetadata(sessionRecord.metadata));
|
|
4206
|
+
}
|
|
4207
|
+
//#endregion
|
|
4208
|
+
//#region src/services/ncp-agent-session-store-adapter.service.ts
|
|
4209
|
+
function readAgentIdFromMetadata(metadata) {
|
|
4210
|
+
return normalizeString(metadata?.agent_id)?.toLowerCase() ?? normalizeString(metadata?.agentId)?.toLowerCase() ?? void 0;
|
|
4211
|
+
}
|
|
4212
|
+
function resolveSessionRecordAgentId(record) {
|
|
4213
|
+
return normalizeString(record.agentId)?.toLowerCase() ?? readAgentIdFromMetadata(record.metadata);
|
|
4214
|
+
}
|
|
4215
|
+
var NcpAgentSessionStoreAdapter = class {
|
|
4216
|
+
constructor(sessionManager, options = {}) {
|
|
4217
|
+
this.sessionManager = sessionManager;
|
|
4218
|
+
this.options = options;
|
|
4219
|
+
}
|
|
4220
|
+
getSession = async (sessionId) => {
|
|
4221
|
+
const session = this.sessionManager.getIfExists(sessionId);
|
|
4222
|
+
if (!session) return null;
|
|
4223
|
+
return {
|
|
4224
|
+
sessionId,
|
|
4225
|
+
...session.agentId ? { agentId: session.agentId } : {},
|
|
4226
|
+
messages: toNcpMessages(sessionId, session.messages),
|
|
4227
|
+
updatedAt: session.updatedAt.toISOString(),
|
|
4228
|
+
metadata: structuredClone(session.metadata)
|
|
4229
|
+
};
|
|
4230
|
+
};
|
|
4231
|
+
listSessions = async () => {
|
|
4232
|
+
const records = this.sessionManager.listSessions();
|
|
4233
|
+
const sessions = [];
|
|
4234
|
+
for (const record of records) {
|
|
4235
|
+
const sessionId = normalizeString(record.key);
|
|
4236
|
+
if (!sessionId) continue;
|
|
4237
|
+
const session = this.sessionManager.getIfExists(sessionId);
|
|
4238
|
+
if (!session) continue;
|
|
4239
|
+
sessions.push({
|
|
4240
|
+
sessionId,
|
|
4241
|
+
...session.agentId ? { agentId: session.agentId } : {},
|
|
4242
|
+
messages: toNcpMessages(sessionId, session.messages),
|
|
4243
|
+
updatedAt: session.updatedAt.toISOString(),
|
|
4244
|
+
metadata: structuredClone(session.metadata)
|
|
4245
|
+
});
|
|
4246
|
+
}
|
|
4247
|
+
sessions.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
4248
|
+
return sessions;
|
|
4249
|
+
};
|
|
4250
|
+
persistSession = async (sessionRecord, options) => {
|
|
4251
|
+
if (this.options.writeMode === "runtime-owned") return;
|
|
4252
|
+
const session = this.sessionManager.getIfExists(sessionRecord.sessionId) ?? this.sessionManager.getOrCreate(sessionRecord.sessionId);
|
|
4253
|
+
const legacyMessages = toLegacyMessages(sessionRecord.messages);
|
|
4254
|
+
const nextAgentId = resolveSessionRecordAgentId(sessionRecord);
|
|
4255
|
+
if (nextAgentId) session.agentId = nextAgentId;
|
|
4256
|
+
session.metadata = resolvePersistedSessionMetadata({
|
|
4257
|
+
currentMetadata: session.metadata,
|
|
4258
|
+
sessionRecord,
|
|
4259
|
+
preserveExistingMetadata: options.preserveExistingMetadata
|
|
4260
|
+
});
|
|
4261
|
+
this.sessionManager.clear(session);
|
|
4262
|
+
for (const message of legacyMessages) this.sessionManager.appendEvent(session, {
|
|
4263
|
+
type: resolveLegacyEventType(message),
|
|
4264
|
+
timestamp: ensureIsoTimestamp(message.timestamp, (/* @__PURE__ */ new Date()).toISOString()),
|
|
4265
|
+
data: { message }
|
|
4266
|
+
});
|
|
4267
|
+
if (legacyMessages.length === 0) session.updatedAt = new Date(ensureIsoTimestamp(sessionRecord.updatedAt, (/* @__PURE__ */ new Date()).toISOString()));
|
|
4268
|
+
this.sessionManager.save(session);
|
|
4269
|
+
this.options.onSessionUpdated?.(sessionRecord.sessionId);
|
|
4270
|
+
};
|
|
4271
|
+
saveSession = async (sessionRecord) => {
|
|
4272
|
+
await this.persistSession(sessionRecord, { preserveExistingMetadata: true });
|
|
4273
|
+
};
|
|
4274
|
+
replaceSession = async (sessionRecord) => {
|
|
4275
|
+
await this.persistSession(sessionRecord, { preserveExistingMetadata: false });
|
|
4276
|
+
};
|
|
4277
|
+
deleteSession = async (sessionId) => {
|
|
4278
|
+
const existing = await this.getSession(sessionId);
|
|
4279
|
+
if (!existing) return null;
|
|
4280
|
+
this.sessionManager.delete(sessionId);
|
|
4281
|
+
this.options.onSessionUpdated?.(sessionId);
|
|
4282
|
+
return existing;
|
|
4283
|
+
};
|
|
4284
|
+
};
|
|
4285
|
+
//#endregion
|
|
225
4286
|
//#region src/app/nextclaw-kernel.ts
|
|
4287
|
+
function resolveKernelSessionsDir(options) {
|
|
4288
|
+
const homeDir = options.homeDir?.trim();
|
|
4289
|
+
if (homeDir) return ensureDir(resolve(expandHome(homeDir), "sessions"));
|
|
4290
|
+
return getSessionsPath();
|
|
4291
|
+
}
|
|
4292
|
+
function resolveKernelAutomationStorePath(options) {
|
|
4293
|
+
const homeDir = options.homeDir?.trim();
|
|
4294
|
+
if (homeDir) return resolve(expandHome(homeDir), "cron", "jobs.json");
|
|
4295
|
+
return resolve(getDataDir(), "cron", "jobs.json");
|
|
4296
|
+
}
|
|
226
4297
|
var NextclawKernelControlManager = class {
|
|
227
4298
|
runtimeControl = null;
|
|
228
4299
|
installRuntimeControl = (runtimeControl) => {
|
|
@@ -234,136 +4305,117 @@ var NextclawKernelControlManager = class {
|
|
|
234
4305
|
};
|
|
235
4306
|
};
|
|
236
4307
|
var NextclawKernel = class {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
contextBuilder = new ContextBuilder(this.sessions);
|
|
241
|
-
control = new NextclawKernelControlManager();
|
|
242
|
-
tools = new ToolManager();
|
|
243
|
-
skills = new SkillManager();
|
|
4308
|
+
eventBus = new EventBus();
|
|
4309
|
+
ingress = new Ingress();
|
|
4310
|
+
messageBus = new MessageBus();
|
|
244
4311
|
llmProviders = new LlmProviderManager();
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
4312
|
+
llmUsage = new LlmUsageManager();
|
|
4313
|
+
configManager;
|
|
4314
|
+
agents;
|
|
4315
|
+
sessions;
|
|
4316
|
+
control;
|
|
4317
|
+
tools;
|
|
4318
|
+
skills;
|
|
4319
|
+
automation;
|
|
4320
|
+
channels;
|
|
4321
|
+
sessionRequests;
|
|
4322
|
+
sessionSearch;
|
|
4323
|
+
ncpSessionApi;
|
|
4324
|
+
extensions;
|
|
4325
|
+
agentRuntimeManager;
|
|
4326
|
+
learningLoop;
|
|
4327
|
+
sessionLifecycleEvents;
|
|
4328
|
+
ncpAgentSessionStore;
|
|
4329
|
+
startPromise = null;
|
|
260
4330
|
constructor(options = {}) {
|
|
261
|
-
|
|
262
|
-
this.
|
|
263
|
-
this.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
4331
|
+
const sessionsDir = resolveKernelSessionsDir(options);
|
|
4332
|
+
this.sessions = new SessionManager({ sessionsDir });
|
|
4333
|
+
this.sessionLifecycleEvents = new NcpLifecycleEventBridge(this.sessions, this.eventBus);
|
|
4334
|
+
this.sessionSearch = new SessionSearchManager({
|
|
4335
|
+
databasePath: resolve(getDataDir(), "session-search.db"),
|
|
4336
|
+
sessionsDir,
|
|
4337
|
+
onSessionUpdated: this.publishSessionUpdated
|
|
4338
|
+
});
|
|
4339
|
+
this.ncpAgentSessionStore = new NcpAgentSessionStoreAdapter(this.sessions, { onSessionUpdated: this.sessionSearch.handleSessionUpdated });
|
|
4340
|
+
this.sessionRequests = new SessionRequestManager({
|
|
4341
|
+
sessions: this.sessions,
|
|
4342
|
+
dispatcher: createAgentRuntimeSessionRequestDispatcher({
|
|
4343
|
+
eventBus: this.eventBus,
|
|
4344
|
+
ingress: this.ingress
|
|
4345
|
+
}),
|
|
4346
|
+
onSessionUpdated: this.sessionSearch.handleSessionUpdated
|
|
4347
|
+
});
|
|
4348
|
+
this.automation = new AutomationManager({ storePath: resolveKernelAutomationStorePath(options) });
|
|
4349
|
+
this.agents = new AgentManager();
|
|
4350
|
+
this.control = new NextclawKernelControlManager();
|
|
4351
|
+
this.tools = new ToolManager();
|
|
4352
|
+
this.skills = new SkillManager();
|
|
4353
|
+
this.extensions = new ExtensionManager();
|
|
4354
|
+
this.channels = new ChannelManager$1({
|
|
4355
|
+
bus: this.messageBus,
|
|
4356
|
+
sessionManager: this.sessions
|
|
4357
|
+
});
|
|
4358
|
+
this.configManager = new ConfigManager({
|
|
4359
|
+
configPath: options.configPath,
|
|
4360
|
+
channels: this.channels,
|
|
4361
|
+
providerManager: this.llmProviders
|
|
4362
|
+
});
|
|
4363
|
+
this.ncpSessionApi = new NcpSessionApiService({
|
|
4364
|
+
eventBus: this.eventBus,
|
|
4365
|
+
getConfig: this.configManager.loadConfig,
|
|
4366
|
+
sessionManager: this.sessions
|
|
4367
|
+
});
|
|
4368
|
+
this.agentRuntimeManager = new AgentRuntimeManager({
|
|
4369
|
+
bus: this.messageBus,
|
|
4370
|
+
providerManager: this.llmProviders,
|
|
4371
|
+
sessions: this.sessions,
|
|
4372
|
+
ingress: this.ingress,
|
|
4373
|
+
sessionRequests: this.sessionRequests,
|
|
4374
|
+
sessionSearch: this.sessionSearch,
|
|
4375
|
+
ncpAgentSessionStore: this.ncpAgentSessionStore,
|
|
4376
|
+
cronService: this.automation,
|
|
4377
|
+
configManager: this.configManager,
|
|
4378
|
+
extensions: this.extensions,
|
|
4379
|
+
eventBus: this.eventBus,
|
|
4380
|
+
handleNcpEvent: this.handleNcpEvent,
|
|
4381
|
+
llmUsage: this.llmUsage,
|
|
4382
|
+
onSessionUpdated: this.publishSessionUpdated
|
|
4383
|
+
});
|
|
4384
|
+
this.learningLoop = new LearningLoopManager({
|
|
4385
|
+
eventBus: this.eventBus,
|
|
4386
|
+
sessionManager: this.sessions,
|
|
4387
|
+
sessionRequester: this.sessionRequests,
|
|
4388
|
+
resolveLearningLoopConfig: () => readLearningLoopRuntimeConfig(this.configManager.loadConfig())
|
|
4389
|
+
});
|
|
4390
|
+
}
|
|
4391
|
+
start = async () => {
|
|
4392
|
+
this.startPromise ??= Promise.resolve().then(() => {
|
|
4393
|
+
this.ncpSessionApi.start();
|
|
4394
|
+
}).then(() => this.sessionSearch.start()).then(() => this.agentRuntimeManager.bootstrap()).then(() => {
|
|
4395
|
+
this.learningLoop.start();
|
|
4396
|
+
}).catch((error) => {
|
|
4397
|
+
this.startPromise = null;
|
|
4398
|
+
throw error;
|
|
4399
|
+
});
|
|
4400
|
+
return this.startPromise;
|
|
332
4401
|
};
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
4402
|
+
dispose = async () => {
|
|
4403
|
+
this.learningLoop.dispose();
|
|
4404
|
+
this.ncpSessionApi.dispose();
|
|
4405
|
+
await this.agentRuntimeManager.dispose();
|
|
4406
|
+
await this.sessionSearch.dispose();
|
|
4407
|
+
this.startPromise = null;
|
|
4408
|
+
};
|
|
4409
|
+
publishSessionUpdated = (sessionKey) => {
|
|
4410
|
+
this.sessionLifecycleEvents.publishSessionUpdated(sessionKey);
|
|
4411
|
+
this.eventBus.emit(eventKeys.sessionUpdated, { sessionKey }, {
|
|
4412
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4413
|
+
source: "backend"
|
|
4414
|
+
});
|
|
4415
|
+
};
|
|
4416
|
+
handleNcpEvent = (event) => {
|
|
4417
|
+
this.sessionLifecycleEvents.handleEndpointEvent(event);
|
|
343
4418
|
};
|
|
344
4419
|
};
|
|
345
4420
|
//#endregion
|
|
346
|
-
|
|
347
|
-
const nextclaw = { eventBus: new EventBus() };
|
|
348
|
-
//#endregion
|
|
349
|
-
//#region src/events/event-keys.config.ts
|
|
350
|
-
function createAppEventKey(id) {
|
|
351
|
-
return { id };
|
|
352
|
-
}
|
|
353
|
-
const eventKeys = {
|
|
354
|
-
configUpdated: createAppEventKey("config.updated"),
|
|
355
|
-
channelConfigApplyStatus: createAppEventKey("channel.config.apply-status"),
|
|
356
|
-
sessionUpdated: createAppEventKey("session.updated"),
|
|
357
|
-
sessionRunStatus: createAppEventKey("session.run-status"),
|
|
358
|
-
sessionSummaryUpsert: createAppEventKey("session.summary.upsert"),
|
|
359
|
-
sessionSummaryDelete: createAppEventKey("session.summary.delete"),
|
|
360
|
-
configReloadStarted: createAppEventKey("config.reload.started"),
|
|
361
|
-
configReloadFinished: createAppEventKey("config.reload.finished"),
|
|
362
|
-
error: createAppEventKey("error"),
|
|
363
|
-
connectionOpen: createAppEventKey("connection.open"),
|
|
364
|
-
connectionClose: createAppEventKey("connection.close"),
|
|
365
|
-
connectionError: createAppEventKey("connection.error"),
|
|
366
|
-
runtimeUpdateSnapshot: createAppEventKey("runtime.update.snapshot")
|
|
367
|
-
};
|
|
368
|
-
//#endregion
|
|
369
|
-
export { AgentManager, AutomationManager, ChannelManager, ContextBuilder, EventBus, LlmProviderManager, NextclawKernel, SessionManager, SkillManager, TaskManager, ToolManager, UpdateManifestReader, createAppEventKey, eventKeys, getUnsignedUpdateManifest, nextclaw, serializeUnsignedUpdateManifest };
|
|
4421
|
+
export { AGENT_RUNTIME_SESSION_MESSAGE_INGRESS_TYPE, AgentManager, AgentRuntimeManager, AgentRuntimeRegistry, AutomationManager, BuiltinNarpRuntimeRegistrationService, ChannelManager, ConfigManager, ContextCompactionPreflightService, DEFAULT_AGENT_RUNTIME_ENTRY_ID, DEFAULT_LEARNING_LOOP_TOOL_CALL_THRESHOLD, ExtensionManager, LEARNING_LOOP_DISABLED_METADATA_KEY, LEARNING_LOOP_LAST_REQUESTED_AT_METADATA_KEY, LEARNING_LOOP_LAST_REVIEW_SESSION_ID_METADATA_KEY, LEARNING_LOOP_LAST_TOOL_CALL_COUNT_METADATA_KEY, LEARNING_LOOP_REQUESTED_SKILLS, LEARNING_LOOP_SOURCE_SESSION_ID_METADATA_KEY, LearningLoopManager, LlmProviderManager, LlmUsageManager, LlmUsageStore, NARP_HTTP_RUNTIME_KIND, NARP_STDIO_RUNTIME_KIND, NativeAgentRuntimeFactory, NcpSessionApiService, NextclawKernel, NextclawNcpContextBuilder, NextclawNcpToolRegistry, PluginRuntimeRegistrationController, ProviderManagerNcpLLMApi, SkillManager, ToolManager, UpdateManifestReader, buildLearningLoopTask, buildLlmUsageSummary, buildNcpUserMessage, buildUserMessageParts, createAgentRuntimeHandle, createAgentRuntimeSessionRequestDispatcher, createAssetTools, createLlmUsageRecord, createNcpSessionSummary, dispatchAgentRuntimeSessionRequest, dispatchChannelReplyRoute, dispatchPromptOverNcp, getUnsignedUpdateManifest, hasLlmUsageTelemetry, isReplyCapableChannel, normalizeAgentRuntimeSessionTypeIcon, normalizeLlmUsageModel, readAccountIdForHints, readLearningLoopRuntimeConfig, resolveAgentHandoffDepth, resolveAgentRuntimeEntries, resolveChannelReplyRoute, resolveLegacyEventType, runGatewayInboundLoop, runPromptOverNcp, sanitizeLlmUsage, serializeUnsignedUpdateManifest, streamPromptOverNcp, toNcpMessages, waitForAgentRuntimeSessionReply };
|