@alisio/core 0.1.0-alpha.5 → 0.1.0-alpha.7
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/application.d.ts +18 -4
- package/dist/application.js +114 -7
- package/dist/config.d.ts +12 -1
- package/dist/config.js +79 -7
- package/dist/core/compaction.d.ts +25 -0
- package/dist/core/compaction.js +54 -0
- package/dist/core/contracts.d.ts +6 -0
- package/dist/core/runner.d.ts +8 -6
- package/dist/core/runner.js +33 -15
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/mcp/connector.d.ts +2 -0
- package/dist/mcp/connector.js +9 -1
- package/dist/plugins/host.js +7 -1
- package/dist/plugins/install.d.ts +100 -0
- package/dist/plugins/install.js +322 -0
- package/dist/resources/context.js +1 -1
- package/dist/runtime/modules.d.ts +3 -1
- package/dist/runtime/modules.js +9 -2
- package/dist/runtime/process.d.ts +9 -0
- package/dist/runtime/process.js +18 -0
- package/dist/runtime/store.d.ts +1 -0
- package/dist/runtime/store.js +9 -0
- package/dist/tools/standard.js +14 -3
- package/dist/version.d.ts +7 -0
- package/dist/version.js +26 -0
- package/package.json +1 -1
package/dist/application.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AvailableProviderModel, ModelInfo, ModelProvider, Plugin, PluginCategory, ResolvedProviderModel, RunEvent } from "@alisio/sdk";
|
|
1
|
+
import type { AvailableProviderModel, ModelInfo, ModelProvider, Plugin, PluginCategory, ProviderConfigurationValue, ResolvedProviderModel, RunEvent } from "@alisio/sdk";
|
|
2
2
|
import type { ApprovalHandler } from "./core/contracts.ts";
|
|
3
3
|
import { ToolRegistry } from "./core/registry.ts";
|
|
4
4
|
import { AgentRunner } from "./core/runner.ts";
|
|
@@ -142,11 +142,25 @@ export declare function createApplication(options?: AppOptions): Promise<{
|
|
|
142
142
|
mcp: McpConnector;
|
|
143
143
|
/** Current-process MCP grant; deliberately independent from persisted server enablement. */
|
|
144
144
|
mcpRuntimePermission(): "granted" | "not-granted" | "read-only";
|
|
145
|
+
/** Whether global MCP consent (`mcp.allow`) is persisted for this user right now. */
|
|
146
|
+
mcpAllowPersisted(): boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Sanitized per-server failures collected while auto-connecting enabled servers at startup
|
|
149
|
+
* (only when runtime permission was granted). Never includes credentials or command paths.
|
|
150
|
+
*/
|
|
151
|
+
mcpStartupFailures(): string[];
|
|
145
152
|
/** Host-owned interactive consent boundary. Headless callers remain blocked unless flagged. */
|
|
146
153
|
grantMcpRuntimePermission(request: {
|
|
147
154
|
source: "interactive-tui";
|
|
148
155
|
confirmed: boolean;
|
|
149
156
|
}): "granted" | "not-granted" | "read-only";
|
|
157
|
+
/** Persist global consent atomically, then grant runtime permission for this process. */
|
|
158
|
+
rememberGlobalMcpConsent(): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* Clear the persisted global consent atomically and drop the runtime permission (disconnecting
|
|
161
|
+
* servers as cleanup does). An explicit this-run `--allow-mcp` flag keeps the grant alive.
|
|
162
|
+
*/
|
|
163
|
+
revokeGlobalMcpConsent(): Promise<void>;
|
|
150
164
|
/** Persist an MCP toggle in its defining layer, then safely update this process. */
|
|
151
165
|
setMcpEnabled(id: string, enabled: boolean, connect?: boolean): Promise<import("./index.ts").McpServerInfo>;
|
|
152
166
|
provider: ActiveProvider;
|
|
@@ -155,7 +169,7 @@ export declare function createApplication(options?: AppOptions): Promise<{
|
|
|
155
169
|
readonly providerError: string | undefined;
|
|
156
170
|
readonly providerInfo: {
|
|
157
171
|
id: string;
|
|
158
|
-
profile: Record<string,
|
|
172
|
+
profile: Record<string, ProviderConfigurationValue>;
|
|
159
173
|
persisted: boolean;
|
|
160
174
|
profileName: string;
|
|
161
175
|
} | {
|
|
@@ -169,8 +183,8 @@ export declare function createApplication(options?: AppOptions): Promise<{
|
|
|
169
183
|
contextWindow: (model: string) => number | undefined;
|
|
170
184
|
/** Effective context budget + compaction point shared by the runner and the TUI bar. */
|
|
171
185
|
contextBudget: (model: string) => {
|
|
172
|
-
total
|
|
173
|
-
basis: "window" | "
|
|
186
|
+
total?: number;
|
|
187
|
+
basis: "window" | "unknown";
|
|
174
188
|
compactionAt: number;
|
|
175
189
|
};
|
|
176
190
|
loadModels: (signal: AbortSignal) => Promise<ModelInfo[]>;
|
package/dist/application.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { configFile, configHome, loadConfigWithProvenance, overridesSavedProviderProfile, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.js";
|
|
3
|
+
import { configFile, configHome, loadConfigWithProvenance, overridesSavedProviderProfile, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.js";
|
|
4
4
|
import { completeText } from "./core/compaction.js";
|
|
5
5
|
import { ToolRegistry } from "./core/registry.js";
|
|
6
6
|
import { AgentRunner } from "./core/runner.js";
|
|
@@ -28,6 +28,17 @@ const nonBlank = (value) => {
|
|
|
28
28
|
const normalized = value?.trim();
|
|
29
29
|
return normalized || undefined;
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Per-profile optional context-window override, in tokens (for local OpenAI-compatible servers
|
|
33
|
+
* like llama.cpp whose `GET /models` omits `context_window`). Numbers are used directly; strings
|
|
34
|
+
* (for example hand-edited providers.json) are parsed. Absent or unparseable means "no override".
|
|
35
|
+
*/
|
|
36
|
+
const parseProfileContextWindow = (value) => {
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
const window = typeof value === "number" ? value : Number(String(value).trim());
|
|
40
|
+
return Number.isFinite(window) && window > 0 ? window : undefined;
|
|
41
|
+
};
|
|
31
42
|
export async function createApplication(options = {}) {
|
|
32
43
|
const cwd = resolve(options.cwd ?? process.cwd()), workspace = await findWorkspace(cwd);
|
|
33
44
|
const { config, provenance: configProvenance } = await loadConfigWithProvenance(workspace, {
|
|
@@ -37,16 +48,20 @@ export async function createApplication(options = {}) {
|
|
|
37
48
|
baseURL: options.baseURL,
|
|
38
49
|
apiMode: options.apiMode,
|
|
39
50
|
});
|
|
51
|
+
// Global-only consent: `mcp.allow` is read from the user layer; the merged config never carries
|
|
52
|
+
// a project value (see loadConfigWithProvenance), so a project cannot grant itself MCP consent.
|
|
53
|
+
const mcpAllow = config.mcp.allow === true;
|
|
54
|
+
let persistedMcpAllow = mcpAllow;
|
|
40
55
|
const store = new SQLiteStore(options.db ?? join(stateHome(), "sessions.sqlite"));
|
|
41
56
|
const registry = new ToolRegistry(), skills = new Skills({ overrides: config.skillOverrides }), context = new ProjectContext(workspace, {
|
|
42
57
|
globalDir: configHome(),
|
|
43
58
|
cwd,
|
|
44
59
|
claudeMdFallback: config.context.claudeMdFallback,
|
|
45
60
|
maxBytes: config.context.maxBytes,
|
|
46
|
-
}), providers = new ProviderRegistry(), plugins = new PluginHost(registry, store, config.pluginHooks, providers), mcp = new McpConnector(config.mcp.servers, registry, workspace, options.readOnly ? "read-only" : options.allowMcp ? "allowed" : "disabled", config.mcpSources);
|
|
61
|
+
}), providers = new ProviderRegistry(), plugins = new PluginHost(registry, store, config.pluginHooks, providers), mcp = new McpConnector(config.mcp.servers, registry, workspace, options.readOnly ? "read-only" : options.allowMcp || mcpAllow ? "allowed" : "disabled", config.mcpSources);
|
|
47
62
|
let mcpRuntimePermission = options.readOnly
|
|
48
63
|
? "read-only"
|
|
49
|
-
: options.allowMcp
|
|
64
|
+
: options.allowMcp || mcpAllow
|
|
50
65
|
? "granted"
|
|
51
66
|
: "not-granted";
|
|
52
67
|
const herdr = options.noHerdr
|
|
@@ -220,7 +235,9 @@ export async function createApplication(options = {}) {
|
|
|
220
235
|
config: config.websearch,
|
|
221
236
|
resolveExtension: () => plugins.extensions.resolve("websearch"),
|
|
222
237
|
});
|
|
223
|
-
|
|
238
|
+
const { registerPluginInstallTool } = await import("./plugins/install.js");
|
|
239
|
+
registerPluginInstallTool(registry, { readOnly: !!options.readOnly });
|
|
240
|
+
if ((options.allowMcp || mcpAllow) && !options.readOnly)
|
|
224
241
|
mcp.register();
|
|
225
242
|
if (options.allowAgents && !options.readOnly)
|
|
226
243
|
herdr.registerTools(registry);
|
|
@@ -297,9 +314,39 @@ export async function createApplication(options = {}) {
|
|
|
297
314
|
throw new Error('websearch.provider is "native" but the active provider does not support native search');
|
|
298
315
|
}
|
|
299
316
|
const models = new Map();
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
// Lazily (re)loads the active provider's catalog exactly once, so the ACTIVE model's
|
|
318
|
+
// contextWindow becomes known for the context bar without waiting for an explicit
|
|
319
|
+
// /model picker or autocomplete call. Failed or absent discovery stays retryable.
|
|
320
|
+
let modelsLoading;
|
|
321
|
+
const ensureModels = () => {
|
|
322
|
+
if (!provider.listModels || modelsLoading)
|
|
323
|
+
return modelsLoading;
|
|
324
|
+
modelsLoading = loadModels(AbortSignal.timeout(10_000));
|
|
325
|
+
modelsLoading.catch(() => {
|
|
326
|
+
if (modelsLoading)
|
|
327
|
+
modelsLoading = undefined;
|
|
328
|
+
});
|
|
329
|
+
return modelsLoading;
|
|
330
|
+
};
|
|
331
|
+
// Priority: explicit configuration for the configured model, then the active saved profile's
|
|
332
|
+
// per-model override (user intent for local servers that omit context_window), then GET /models.
|
|
333
|
+
const contextWindow = (model) => {
|
|
334
|
+
const explicit = model === config.provider.model ? config.provider.contextWindow : undefined;
|
|
335
|
+
if (explicit !== undefined)
|
|
336
|
+
return explicit;
|
|
337
|
+
const override = providerInfo?.persisted && model === provider.model
|
|
338
|
+
? parseProfileContextWindow(providerInfo.profile.contextWindow)
|
|
339
|
+
: undefined;
|
|
340
|
+
if (override !== undefined)
|
|
341
|
+
return override;
|
|
342
|
+
const known = models.get(model)?.contextWindow;
|
|
343
|
+
if (known !== undefined)
|
|
344
|
+
return known;
|
|
345
|
+
// Background prime: this call stays synchronous (the runner and the bar are sync), and
|
|
346
|
+
// once the catalog lands the map is populated for every later lookup.
|
|
347
|
+
void ensureModels();
|
|
348
|
+
return undefined;
|
|
349
|
+
};
|
|
303
350
|
const loadModels = async (signal) => {
|
|
304
351
|
if (!provider.listModels)
|
|
305
352
|
return [];
|
|
@@ -431,6 +478,7 @@ export async function createApplication(options = {}) {
|
|
|
431
478
|
external: !options.readOnly &&
|
|
432
479
|
(!!options.allowExternal ||
|
|
433
480
|
!!options.allowMcp ||
|
|
481
|
+
mcpAllow ||
|
|
434
482
|
!!options.allowAgents ||
|
|
435
483
|
plugins.externalCount > 0 ||
|
|
436
484
|
registry.list().some((t) => t.name.startsWith("p_"))),
|
|
@@ -508,6 +556,24 @@ export async function createApplication(options = {}) {
|
|
|
508
556
|
}
|
|
509
557
|
return expanded;
|
|
510
558
|
};
|
|
559
|
+
// Startup auto-connect: when runtime permission is granted (--allow-mcp or global mcp.allow),
|
|
560
|
+
// connect every enabled server exactly like pressing Connect for each. Failures are per-server,
|
|
561
|
+
// sanitized and never fatal to startup.
|
|
562
|
+
const mcpStartupFailures = [];
|
|
563
|
+
if (mcpRuntimePermission === "granted") {
|
|
564
|
+
for (const [name, server] of Object.entries(config.mcp.servers)) {
|
|
565
|
+
if (!server.enabled)
|
|
566
|
+
continue;
|
|
567
|
+
try {
|
|
568
|
+
await mcp.connect(name, AbortSignal.timeout(15_000));
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
const diagnostic = mcp.info(name).diagnostic ??
|
|
572
|
+
"Connection failed; inspect the server separately for details";
|
|
573
|
+
mcpStartupFailures.push(`MCP server "${name}" did not auto-connect: ${diagnostic}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
511
577
|
return {
|
|
512
578
|
workspace,
|
|
513
579
|
config,
|
|
@@ -595,6 +661,17 @@ export async function createApplication(options = {}) {
|
|
|
595
661
|
mcpRuntimePermission() {
|
|
596
662
|
return mcpRuntimePermission;
|
|
597
663
|
},
|
|
664
|
+
/** Whether global MCP consent (`mcp.allow`) is persisted for this user right now. */
|
|
665
|
+
mcpAllowPersisted() {
|
|
666
|
+
return persistedMcpAllow;
|
|
667
|
+
},
|
|
668
|
+
/**
|
|
669
|
+
* Sanitized per-server failures collected while auto-connecting enabled servers at startup
|
|
670
|
+
* (only when runtime permission was granted). Never includes credentials or command paths.
|
|
671
|
+
*/
|
|
672
|
+
mcpStartupFailures() {
|
|
673
|
+
return [...mcpStartupFailures];
|
|
674
|
+
},
|
|
598
675
|
/** Host-owned interactive consent boundary. Headless callers remain blocked unless flagged. */
|
|
599
676
|
grantMcpRuntimePermission(request) {
|
|
600
677
|
if (!request.confirmed)
|
|
@@ -610,6 +687,35 @@ export async function createApplication(options = {}) {
|
|
|
610
687
|
mcpRuntimePermission = "granted";
|
|
611
688
|
return mcpRuntimePermission;
|
|
612
689
|
},
|
|
690
|
+
/** Persist global consent atomically, then grant runtime permission for this process. */
|
|
691
|
+
async rememberGlobalMcpConsent() {
|
|
692
|
+
if (options.readOnly)
|
|
693
|
+
throw new Error("MCP is unavailable under --read-only");
|
|
694
|
+
await setGlobalMcpAllow({ allow: true });
|
|
695
|
+
persistedMcpAllow = true;
|
|
696
|
+
this.grantMcpRuntimePermission({ source: "interactive-tui", confirmed: true });
|
|
697
|
+
},
|
|
698
|
+
/**
|
|
699
|
+
* Clear the persisted global consent atomically and drop the runtime permission (disconnecting
|
|
700
|
+
* servers as cleanup does). An explicit this-run `--allow-mcp` flag keeps the grant alive.
|
|
701
|
+
*/
|
|
702
|
+
async revokeGlobalMcpConsent() {
|
|
703
|
+
if (options.readOnly)
|
|
704
|
+
throw new Error("MCP is unavailable under --read-only");
|
|
705
|
+
await setGlobalMcpAllow({ allow: false });
|
|
706
|
+
persistedMcpAllow = false;
|
|
707
|
+
if (options.allowMcp)
|
|
708
|
+
return;
|
|
709
|
+
if (mcpRuntimePermission !== "granted")
|
|
710
|
+
return;
|
|
711
|
+
await mcp.revokeRuntimePermission();
|
|
712
|
+
mcpRuntimePermission = "not-granted";
|
|
713
|
+
runtimePolicy.external =
|
|
714
|
+
!!options.allowExternal ||
|
|
715
|
+
!!options.allowAgents ||
|
|
716
|
+
plugins.externalCount > 0 ||
|
|
717
|
+
registry.list().some((t) => t.name.startsWith("p_"));
|
|
718
|
+
},
|
|
613
719
|
/** Persist an MCP toggle in its defining layer, then safely update this process. */
|
|
614
720
|
async setMcpEnabled(id, enabled, connect = false) {
|
|
615
721
|
if (options.readOnly)
|
|
@@ -688,6 +794,7 @@ export async function createApplication(options = {}) {
|
|
|
688
794
|
providerError = undefined;
|
|
689
795
|
providerInfo = { id, profile, persisted: true, profileName };
|
|
690
796
|
models.clear();
|
|
797
|
+
modelsLoading = undefined;
|
|
691
798
|
catalogCache = undefined;
|
|
692
799
|
}
|
|
693
800
|
catch (error) {
|
package/dist/config.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
85
85
|
enabled: boolean;
|
|
86
86
|
bearerTokenEnv?: string | undefined;
|
|
87
87
|
}>>]>>>;
|
|
88
|
+
allow: z.ZodOptional<z.ZodBoolean>;
|
|
88
89
|
}, z.core.$strict>>;
|
|
89
90
|
mcpServers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
90
91
|
transport: z.ZodLiteral<"stdio">;
|
|
@@ -223,6 +224,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
223
224
|
maxTokens?: number | undefined;
|
|
224
225
|
};
|
|
225
226
|
mcp: {
|
|
227
|
+
allow?: boolean | undefined;
|
|
226
228
|
servers: {
|
|
227
229
|
[x: string]: {
|
|
228
230
|
transport: "stdio";
|
|
@@ -294,6 +296,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
294
296
|
bearerTokenEnv?: string | undefined;
|
|
295
297
|
transport: "http";
|
|
296
298
|
}>;
|
|
299
|
+
allow?: boolean | undefined;
|
|
297
300
|
};
|
|
298
301
|
mcpServers?: Record<string, {
|
|
299
302
|
transport: "stdio";
|
|
@@ -376,7 +379,11 @@ export interface ConfigLoadResult {
|
|
|
376
379
|
config: LoadedConfig;
|
|
377
380
|
provenance: ConfigProvenance;
|
|
378
381
|
}
|
|
379
|
-
/**
|
|
382
|
+
/**
|
|
383
|
+
* Whether legacy provider selection should take priority over a saved plugin profile for this
|
|
384
|
+
* run: an explicit endpoint override, or a trusted project/explicit layer whose parsed root
|
|
385
|
+
* `provider.model` is actually usable (non-empty and not the `alisio setup` placeholder).
|
|
386
|
+
*/
|
|
380
387
|
export declare function overridesSavedProviderProfile(provenance: ConfigProvenance, hasEndpointOverride: boolean): boolean;
|
|
381
388
|
export declare const configHome: () => string;
|
|
382
389
|
export declare const stateHome: () => string;
|
|
@@ -405,6 +412,10 @@ export declare function setMcpServerEnabled(input: {
|
|
|
405
412
|
name: string;
|
|
406
413
|
enabled: boolean;
|
|
407
414
|
}): Promise<string>;
|
|
415
|
+
/** Atomically sets or clears the global `mcp.allow` consent preference in the user config file. */
|
|
416
|
+
export declare function setGlobalMcpAllow(input: {
|
|
417
|
+
allow: boolean;
|
|
418
|
+
}): Promise<string>;
|
|
408
419
|
/** Atomically updates one project plugin override while preserving every unrelated JSON field. */
|
|
409
420
|
export declare function setProjectPluginEnabled(input: {
|
|
410
421
|
workspace: string;
|
package/dist/config.js
CHANGED
|
@@ -84,7 +84,14 @@ const configObjectSchema = z
|
|
|
84
84
|
/** Project-local enable/disable overrides for effective non-plugin skills. */
|
|
85
85
|
skillOverrides: z.record(z.string(), z.object({ enabled: z.boolean() }).strict()).default({}),
|
|
86
86
|
mcp: z
|
|
87
|
-
.object({
|
|
87
|
+
.object({
|
|
88
|
+
servers: serversSchema(compatibleServerSchema).default({}),
|
|
89
|
+
/**
|
|
90
|
+
* Global-only user preference: grants MCP process/network consent across sessions.
|
|
91
|
+
* Only the user (global) configuration layer is consulted; a project value is ignored.
|
|
92
|
+
*/
|
|
93
|
+
allow: z.boolean().optional(),
|
|
94
|
+
})
|
|
88
95
|
.strict()
|
|
89
96
|
.default({ servers: {} }),
|
|
90
97
|
/** Compatibility with the common MCP client configuration shape. */
|
|
@@ -172,9 +179,24 @@ const configObjectSchema = z
|
|
|
172
179
|
});
|
|
173
180
|
export const configSchema = configObjectSchema.transform(({ mcpServers, ...config }) => ({
|
|
174
181
|
...config,
|
|
175
|
-
mcp: { servers: { ...config.mcp.servers, ...mcpServers } },
|
|
182
|
+
mcp: { ...config.mcp, servers: { ...config.mcp.servers, ...mcpServers } },
|
|
176
183
|
}));
|
|
177
|
-
/**
|
|
184
|
+
/** The `alisio setup` scaffold writes this placeholder; a model matching it is not usable. */
|
|
185
|
+
const LEGACY_PROVIDER_MODEL_PLACEHOLDER = "YOUR_MODEL_ID";
|
|
186
|
+
/**
|
|
187
|
+
* A legacy root `provider` is usable only when its parsed model is non-empty and not the
|
|
188
|
+
* `alisio setup` placeholder, so a scaffolded or empty project config never defeats a saved
|
|
189
|
+
* `/connect` profile.
|
|
190
|
+
*/
|
|
191
|
+
function isUsableLegacyModel(model) {
|
|
192
|
+
const normalized = model.trim();
|
|
193
|
+
return normalized !== "" && normalized !== LEGACY_PROVIDER_MODEL_PLACEHOLDER;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Whether legacy provider selection should take priority over a saved plugin profile for this
|
|
197
|
+
* run: an explicit endpoint override, or a trusted project/explicit layer whose parsed root
|
|
198
|
+
* `provider.model` is actually usable (non-empty and not the `alisio setup` placeholder).
|
|
199
|
+
*/
|
|
178
200
|
export function overridesSavedProviderProfile(provenance, hasEndpointOverride) {
|
|
179
201
|
return hasEndpointOverride || !!provenance.selectedLayer?.hasLegacyProvider;
|
|
180
202
|
}
|
|
@@ -233,10 +255,12 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
|
233
255
|
global.config.skillOverrides = {};
|
|
234
256
|
let config = global.config;
|
|
235
257
|
let mcpSources = global.sources;
|
|
236
|
-
|
|
258
|
+
// Parsed `provider.model` of the selected layer itself (before env/CLI overrides), used to
|
|
259
|
+
// decide whether its legacy root `provider` is usable against a saved `/connect` profile.
|
|
260
|
+
let selectedLegacyModel = "";
|
|
237
261
|
if (selectedFile && selectedFile !== globalFile) {
|
|
238
262
|
const selected = await parseLayer(selectedFile, options.file ? "explicit" : "project");
|
|
239
|
-
|
|
263
|
+
selectedLegacyModel = selected.config.provider.model;
|
|
240
264
|
const overlaid = { ...config };
|
|
241
265
|
for (const key of selected.keys) {
|
|
242
266
|
if (key === "mcp" || key === "mcpServers")
|
|
@@ -245,14 +269,17 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
|
245
269
|
overlaid[key] = selected.config[key];
|
|
246
270
|
}
|
|
247
271
|
if (selected.keys.has("mcp") || selected.keys.has("mcpServers"))
|
|
272
|
+
// Only servers merge upward; `mcp.allow` is a global/user preference and is deliberately
|
|
273
|
+
// dropped from the selected layer so a project can never grant itself network consent.
|
|
248
274
|
overlaid.mcp = {
|
|
275
|
+
...config.mcp,
|
|
249
276
|
servers: { ...config.mcp.servers, ...selected.config.mcp.servers },
|
|
250
277
|
};
|
|
251
278
|
mcpSources = { ...mcpSources, ...selected.sources };
|
|
252
279
|
config = overlaid;
|
|
253
280
|
}
|
|
254
281
|
else if (selectedFile) {
|
|
255
|
-
|
|
282
|
+
selectedLegacyModel = global.config.provider.model;
|
|
256
283
|
}
|
|
257
284
|
config.provider = providerSchema.parse({
|
|
258
285
|
...config.provider,
|
|
@@ -273,7 +300,7 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
|
273
300
|
? {
|
|
274
301
|
selectedLayer: {
|
|
275
302
|
kind: options.file ? "explicit" : "project",
|
|
276
|
-
hasLegacyProvider:
|
|
303
|
+
hasLegacyProvider: isUsableLegacyModel(selectedLegacyModel),
|
|
277
304
|
},
|
|
278
305
|
}
|
|
279
306
|
: {}),
|
|
@@ -326,6 +353,51 @@ export async function setMcpServerEnabled(input) {
|
|
|
326
353
|
}
|
|
327
354
|
return file;
|
|
328
355
|
}
|
|
356
|
+
/** Atomically sets or clears the global `mcp.allow` consent preference in the user config file. */
|
|
357
|
+
export async function setGlobalMcpAllow(input) {
|
|
358
|
+
const file = join(configHome(), "config.json");
|
|
359
|
+
let raw = {};
|
|
360
|
+
if (await exists(file)) {
|
|
361
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
362
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
363
|
+
throw new Error("Global Alisio configuration must be a JSON object");
|
|
364
|
+
raw = parsed;
|
|
365
|
+
}
|
|
366
|
+
if (input.allow) {
|
|
367
|
+
const current = raw.mcp && typeof raw.mcp === "object" && !Array.isArray(raw.mcp)
|
|
368
|
+
? raw.mcp
|
|
369
|
+
: {};
|
|
370
|
+
raw.mcp = { ...current, allow: true };
|
|
371
|
+
}
|
|
372
|
+
else if (raw.mcp && typeof raw.mcp === "object" && !Array.isArray(raw.mcp)) {
|
|
373
|
+
const mcp = { ...raw.mcp };
|
|
374
|
+
delete mcp.allow;
|
|
375
|
+
if (Object.keys(mcp).length)
|
|
376
|
+
raw.mcp = mcp;
|
|
377
|
+
else
|
|
378
|
+
delete raw.mcp;
|
|
379
|
+
}
|
|
380
|
+
raw.schemaVersion ??= 1;
|
|
381
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
382
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
383
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
384
|
+
try {
|
|
385
|
+
await handle.writeFile(`${JSON.stringify(raw, null, 2)}\n`, "utf8");
|
|
386
|
+
await handle.sync();
|
|
387
|
+
}
|
|
388
|
+
finally {
|
|
389
|
+
await handle.close();
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
await rename(temporary, file);
|
|
393
|
+
await chmod(file, 0o600);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
await unlink(temporary).catch(() => { });
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
return file;
|
|
400
|
+
}
|
|
329
401
|
/** Atomically updates one project plugin override while preserving every unrelated JSON field. */
|
|
330
402
|
export async function setProjectPluginEnabled(input) {
|
|
331
403
|
const file = join(input.workspace, ".alisio", "config.json");
|
|
@@ -44,6 +44,31 @@ export interface CompactionPlan {
|
|
|
44
44
|
kept: Message[];
|
|
45
45
|
}
|
|
46
46
|
export declare function planCompaction(messages: Message[], options?: CompactionOptions): CompactionPlan | undefined;
|
|
47
|
+
/** Marker appended to message content cut by the context-budget reduction. */
|
|
48
|
+
export declare const TRUNCATION_MARKER = "\u2026 [truncated by context budget]";
|
|
49
|
+
/** Default per-message cap for kept tool result text after a failed compaction. */
|
|
50
|
+
export declare const MAX_TOOL_RESULT_CHARS = 8000;
|
|
51
|
+
/** Default per-message cap for kept user/assistant text after a failed compaction. */
|
|
52
|
+
export declare const MAX_TEXT_CHARS = 16000;
|
|
53
|
+
export interface MessageReductionOptions {
|
|
54
|
+
/** Keep at most this many characters of text per tool result (default 8_000). */
|
|
55
|
+
maxToolResultChars?: number;
|
|
56
|
+
/** Keep at most this many characters per user/assistant text (default 16_000). */
|
|
57
|
+
maxTextChars?: number;
|
|
58
|
+
}
|
|
59
|
+
export interface MessageReduction {
|
|
60
|
+
messages: Message[];
|
|
61
|
+
/** Number of messages whose content was truncated. */
|
|
62
|
+
truncated: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Cuts oversized retained content so the session fits the context-budget hard limit after a
|
|
66
|
+
* compaction could not. Only message CONTENT changes (user/assistant `text` and the text parts
|
|
67
|
+
* of tool results); roles, call IDs, order and message boundaries stay identical, so the
|
|
68
|
+
* transcript remains valid and replayable and a tool call is never separated from its results.
|
|
69
|
+
* Checkpoint summaries (`summary: true`) are bounded by design and never truncated.
|
|
70
|
+
*/
|
|
71
|
+
export declare function reduceMessageSizes(messages: Message[], options?: MessageReductionOptions): MessageReduction;
|
|
47
72
|
/**
|
|
48
73
|
* Plain-text transcript used as summarizer input. Provider continuation data (for example
|
|
49
74
|
* encrypted reasoning) is opaque and is intentionally not included.
|
package/dist/core/compaction.js
CHANGED
|
@@ -91,6 +91,60 @@ export function planCompaction(messages, options = {}) {
|
|
|
91
91
|
return { cut, summarized: messages.slice(0, cut), kept: messages.slice(cut) };
|
|
92
92
|
}
|
|
93
93
|
const clip = (text, max) => text.length > max ? `${text.slice(0, max)}… [truncated]` : text;
|
|
94
|
+
/** Marker appended to message content cut by the context-budget reduction. */
|
|
95
|
+
export const TRUNCATION_MARKER = "… [truncated by context budget]";
|
|
96
|
+
/** Default per-message cap for kept tool result text after a failed compaction. */
|
|
97
|
+
export const MAX_TOOL_RESULT_CHARS = 8_000;
|
|
98
|
+
/** Default per-message cap for kept user/assistant text after a failed compaction. */
|
|
99
|
+
export const MAX_TEXT_CHARS = 16_000;
|
|
100
|
+
/**
|
|
101
|
+
* Cuts oversized retained content so the session fits the context-budget hard limit after a
|
|
102
|
+
* compaction could not. Only message CONTENT changes (user/assistant `text` and the text parts
|
|
103
|
+
* of tool results); roles, call IDs, order and message boundaries stay identical, so the
|
|
104
|
+
* transcript remains valid and replayable and a tool call is never separated from its results.
|
|
105
|
+
* Checkpoint summaries (`summary: true`) are bounded by design and never truncated.
|
|
106
|
+
*/
|
|
107
|
+
export function reduceMessageSizes(messages, options = {}) {
|
|
108
|
+
const maxToolResultChars = options.maxToolResultChars ?? MAX_TOOL_RESULT_CHARS;
|
|
109
|
+
const maxTextChars = options.maxTextChars ?? MAX_TEXT_CHARS;
|
|
110
|
+
let truncated = 0;
|
|
111
|
+
const reduced = messages.map((m) => {
|
|
112
|
+
if (m.role === "user") {
|
|
113
|
+
if (m.summary || m.text.length <= maxTextChars)
|
|
114
|
+
return m;
|
|
115
|
+
truncated++;
|
|
116
|
+
return { ...m, text: `${m.text.slice(0, maxTextChars)}\n${TRUNCATION_MARKER}` };
|
|
117
|
+
}
|
|
118
|
+
if (m.role === "assistant") {
|
|
119
|
+
if (m.text.length <= maxTextChars)
|
|
120
|
+
return m;
|
|
121
|
+
truncated++;
|
|
122
|
+
return { ...m, text: `${m.text.slice(0, maxTextChars)}\n${TRUNCATION_MARKER}` };
|
|
123
|
+
}
|
|
124
|
+
let used = 0;
|
|
125
|
+
let changed = false;
|
|
126
|
+
const content = m.result.content.map((part) => {
|
|
127
|
+
const room = maxToolResultChars - used;
|
|
128
|
+
const text = room <= 0
|
|
129
|
+
? TRUNCATION_MARKER
|
|
130
|
+
: part.text.length > room
|
|
131
|
+
? `${part.text.slice(0, room)}\n${TRUNCATION_MARKER}`
|
|
132
|
+
: part.text;
|
|
133
|
+
if (text !== part.text) {
|
|
134
|
+
changed = true;
|
|
135
|
+
used = maxToolResultChars;
|
|
136
|
+
}
|
|
137
|
+
else
|
|
138
|
+
used += part.text.length;
|
|
139
|
+
return { type: "text", text };
|
|
140
|
+
});
|
|
141
|
+
if (!changed)
|
|
142
|
+
return m;
|
|
143
|
+
truncated++;
|
|
144
|
+
return { ...m, result: { ...m.result, content } };
|
|
145
|
+
});
|
|
146
|
+
return truncated ? { messages: reduced, truncated } : { messages, truncated: 0 };
|
|
147
|
+
}
|
|
94
148
|
/**
|
|
95
149
|
* Plain-text transcript used as summarizer input. Provider continuation data (for example
|
|
96
150
|
* encrypted reasoning) is opaque and is intentionally not included.
|
package/dist/core/contracts.d.ts
CHANGED
|
@@ -56,6 +56,12 @@ export interface SessionStore {
|
|
|
56
56
|
* rest verbatim and in order. Callers must never split a tool call from its results.
|
|
57
57
|
*/
|
|
58
58
|
compact(id: string, replaced: number, summary: Message): void;
|
|
59
|
+
/**
|
|
60
|
+
* Atomically replace the whole active history with `messages` (like `compact` without a
|
|
61
|
+
* summary): used after a context-budget reduction so the persisted transcript matches what
|
|
62
|
+
* was actually sent. Callers must never split a tool call from its results.
|
|
63
|
+
*/
|
|
64
|
+
overwrite(id: string, messages: Message[]): void;
|
|
59
65
|
acquire(id: string): void;
|
|
60
66
|
release(id: string): void;
|
|
61
67
|
beginCall(session: string, call: ToolCall): void;
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -108,14 +108,16 @@ export declare class AgentRunner {
|
|
|
108
108
|
/** Estimated tokens the next request of a session would send. */
|
|
109
109
|
estimateContext(sessionId: string): Promise<number>;
|
|
110
110
|
/**
|
|
111
|
-
* The effective context budget
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* compaction triggers, so the bar turns red exactly where the engine
|
|
111
|
+
* The effective context budget the TUI context bar displays. The model window is the total
|
|
112
|
+
* when known (and not beyond `MAX_TRUSTED_WINDOW`); otherwise the bar reports an honest
|
|
113
|
+
* unknown (`basis: "unknown"`, no fabricated total). `compactionAt` is the percentage of
|
|
114
|
+
* `total` where auto-compaction triggers, so the bar turns red exactly where the engine
|
|
115
|
+
* compacts. The engine's own auto-compaction guardrail still falls back to the char budget
|
|
116
|
+
* internally (see `shouldCompactContext`) — that fallback is never shown as a fake total.
|
|
115
117
|
*/
|
|
116
118
|
contextBudget(model: string): {
|
|
117
|
-
total
|
|
118
|
-
basis: "window" | "
|
|
119
|
+
total?: number;
|
|
120
|
+
basis: "window" | "unknown";
|
|
119
121
|
compactionAt: number;
|
|
120
122
|
};
|
|
121
123
|
/** Change the model used by subsequent turns of a session; recorded in the store. */
|
package/dist/core/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { textResult, } from "@alisio/sdk";
|
|
2
|
-
import { checkpointInstructions,
|
|
2
|
+
import { checkpointInstructions, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, reduceMessageSizes, shouldCompactContext, summarize, summaryMessage, } from "./compaction.js";
|
|
3
3
|
/**
|
|
4
4
|
* Default cumulative token budget for one run: several context windows (each turn re-sends the
|
|
5
5
|
* context), clamped; 1M tokens when the window is unknown.
|
|
@@ -112,19 +112,22 @@ export class AgentRunner {
|
|
|
112
112
|
estimateTokens(this.toolsText(this.availableTools())));
|
|
113
113
|
}
|
|
114
114
|
/**
|
|
115
|
-
* The effective context budget
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* compaction triggers, so the bar turns red exactly where the engine
|
|
115
|
+
* The effective context budget the TUI context bar displays. The model window is the total
|
|
116
|
+
* when known (and not beyond `MAX_TRUSTED_WINDOW`); otherwise the bar reports an honest
|
|
117
|
+
* unknown (`basis: "unknown"`, no fabricated total). `compactionAt` is the percentage of
|
|
118
|
+
* `total` where auto-compaction triggers, so the bar turns red exactly where the engine
|
|
119
|
+
* compacts. The engine's own auto-compaction guardrail still falls back to the char budget
|
|
120
|
+
* internally (see `shouldCompactContext`) — that fallback is never shown as a fake total.
|
|
119
121
|
*/
|
|
120
122
|
contextBudget(model) {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
: 100,
|
|
127
|
-
|
|
123
|
+
const window = this.options.contextWindow?.(model);
|
|
124
|
+
if (window !== undefined && window > 0 && window <= MAX_TRUSTED_WINDOW)
|
|
125
|
+
return {
|
|
126
|
+
total: window,
|
|
127
|
+
basis: "window",
|
|
128
|
+
compactionAt: Math.round((this.options.compaction?.threshold ?? 0.85) * 100),
|
|
129
|
+
};
|
|
130
|
+
return { total: undefined, basis: "unknown", compactionAt: 100 };
|
|
128
131
|
}
|
|
129
132
|
/** Change the model used by subsequent turns of a session; recorded in the store. */
|
|
130
133
|
setModel(sessionId, model) {
|
|
@@ -260,6 +263,7 @@ export class AgentRunner {
|
|
|
260
263
|
async run(sessionId, prompt, signal, options = {}) {
|
|
261
264
|
const controller = this.claim(sessionId);
|
|
262
265
|
const o = this.options, emit = this.emitter(sessionId);
|
|
266
|
+
const limit = o.maxContextChars ?? 160_000;
|
|
263
267
|
let tokens = 0, lastText = "", acquired = false;
|
|
264
268
|
const usageTotal = { input: 0, output: 0 };
|
|
265
269
|
const timeout = AbortSignal.timeout(options.timeoutMs ?? o.timeoutMs ?? 300_000);
|
|
@@ -348,13 +352,27 @@ export class AgentRunner {
|
|
|
348
352
|
// estimate (`chars / 4 >= maxContextChars / 4`). `maxContextChars` below stays as the
|
|
349
353
|
// post-compaction hard limit.
|
|
350
354
|
if (compaction.auto !== false &&
|
|
351
|
-
shouldCompactContext(used(), chars(), o.contextWindow?.(model),
|
|
355
|
+
shouldCompactContext(used(), chars(), o.contextWindow?.(model), limit, compaction.threshold ?? 0.85)) {
|
|
352
356
|
await this.compactLocked(sessionId, emit, model, combined, "auto");
|
|
353
357
|
instructions = await withPersona();
|
|
354
358
|
messages = o.store.messages(sessionId);
|
|
355
359
|
}
|
|
356
|
-
|
|
357
|
-
|
|
360
|
+
// A compaction that still leaves the session over the hard limit (typically a kept tail
|
|
361
|
+
// holding huge tool outputs) is reduced in place instead of failing outright: oversized
|
|
362
|
+
// kept content is cut per message (roles/callIds/boundaries untouched, so the transcript
|
|
363
|
+
// stays valid and replayable). Only if even the reduced tail exceeds the limit the run
|
|
364
|
+
// fails with an actionable error; the reduction is persisted either way, so the session
|
|
365
|
+
// stays usable for later prompts.
|
|
366
|
+
if (chars() > limit) {
|
|
367
|
+
const reduction = reduceMessageSizes(messages);
|
|
368
|
+
if (reduction.truncated > 0) {
|
|
369
|
+
o.store.overwrite(sessionId, reduction.messages);
|
|
370
|
+
messages = reduction.messages;
|
|
371
|
+
emit("context_reduced", { messages: reduction.truncated });
|
|
372
|
+
}
|
|
373
|
+
if (chars() > limit)
|
|
374
|
+
throw new Error(`Context budget exceeded and compaction could not reduce it (approximately ${chars()} characters; limit ${limit}). Run /compact, trim large tool outputs, or start a new session.`);
|
|
375
|
+
}
|
|
358
376
|
let completion;
|
|
359
377
|
let truncated = false;
|
|
360
378
|
let usage;
|