@alisio/core 0.1.0-alpha.3 → 0.1.0-alpha.6
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 +22 -2
- package/dist/application.js +116 -7
- package/dist/config.d.ts +15 -1
- package/dist/config.js +85 -8
- package/dist/core/compaction.d.ts +36 -1
- package/dist/core/compaction.js +66 -17
- package/dist/core/runner.d.ts +20 -1
- package/dist/core/runner.js +43 -8
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- 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/tools/standard.js +14 -3
- package/dist/version.d.ts +7 -0
- package/dist/version.js +26 -0
- package/package.json +2 -2
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
|
} | {
|
|
@@ -167,6 +181,12 @@ export declare function createApplication(options?: AppOptions): Promise<{
|
|
|
167
181
|
runner: AgentRunner;
|
|
168
182
|
herdr: HerdrBridge;
|
|
169
183
|
contextWindow: (model: string) => number | undefined;
|
|
184
|
+
/** Effective context budget + compaction point shared by the runner and the TUI bar. */
|
|
185
|
+
contextBudget: (model: string) => {
|
|
186
|
+
total?: number;
|
|
187
|
+
basis: "window" | "unknown";
|
|
188
|
+
compactionAt: number;
|
|
189
|
+
};
|
|
170
190
|
loadModels: (signal: AbortSignal) => Promise<ModelInfo[]>;
|
|
171
191
|
/** Creates a provider for validation/model discovery without changing the active provider. */
|
|
172
192
|
probeProvider(id: string, profile: Record<string, import("@alisio/sdk").ProviderConfigurationValue>, credentials: Record<string, string>, 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)
|
|
@@ -635,6 +741,8 @@ export async function createApplication(options = {}) {
|
|
|
635
741
|
runner,
|
|
636
742
|
herdr,
|
|
637
743
|
contextWindow,
|
|
744
|
+
/** Effective context budget + compaction point shared by the runner and the TUI bar. */
|
|
745
|
+
contextBudget: (model) => runner.contextBudget(model),
|
|
638
746
|
loadModels,
|
|
639
747
|
/** Creates a provider for validation/model discovery without changing the active provider. */
|
|
640
748
|
async probeProvider(id, profile, credentials, signal) {
|
|
@@ -686,6 +794,7 @@ export async function createApplication(options = {}) {
|
|
|
686
794
|
providerError = undefined;
|
|
687
795
|
providerInfo = { id, profile, persisted: true, profileName };
|
|
688
796
|
models.clear();
|
|
797
|
+
modelsLoading = undefined;
|
|
689
798
|
catalogCache = undefined;
|
|
690
799
|
}
|
|
691
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">;
|
|
@@ -149,6 +150,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
149
150
|
auto: z.ZodDefault<z.ZodBoolean>;
|
|
150
151
|
threshold: z.ZodDefault<z.ZodNumber>;
|
|
151
152
|
keepTurns: z.ZodDefault<z.ZodNumber>;
|
|
153
|
+
maxOutputTokens: z.ZodDefault<z.ZodNumber>;
|
|
152
154
|
}, z.core.$strict>>;
|
|
153
155
|
websearch: z.ZodDefault<z.ZodObject<{
|
|
154
156
|
provider: z.ZodOptional<z.ZodEnum<{
|
|
@@ -206,6 +208,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
206
208
|
auto: boolean;
|
|
207
209
|
threshold: number;
|
|
208
210
|
keepTurns: number;
|
|
211
|
+
maxOutputTokens: number;
|
|
209
212
|
};
|
|
210
213
|
websearch: {
|
|
211
214
|
provider?: "brave" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
|
|
@@ -221,6 +224,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
221
224
|
maxTokens?: number | undefined;
|
|
222
225
|
};
|
|
223
226
|
mcp: {
|
|
227
|
+
allow?: boolean | undefined;
|
|
224
228
|
servers: {
|
|
225
229
|
[x: string]: {
|
|
226
230
|
transport: "stdio";
|
|
@@ -292,6 +296,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
292
296
|
bearerTokenEnv?: string | undefined;
|
|
293
297
|
transport: "http";
|
|
294
298
|
}>;
|
|
299
|
+
allow?: boolean | undefined;
|
|
295
300
|
};
|
|
296
301
|
mcpServers?: Record<string, {
|
|
297
302
|
transport: "stdio";
|
|
@@ -337,6 +342,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
337
342
|
auto: boolean;
|
|
338
343
|
threshold: number;
|
|
339
344
|
keepTurns: number;
|
|
345
|
+
maxOutputTokens: number;
|
|
340
346
|
};
|
|
341
347
|
websearch: {
|
|
342
348
|
provider?: "brave" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
|
|
@@ -373,7 +379,11 @@ export interface ConfigLoadResult {
|
|
|
373
379
|
config: LoadedConfig;
|
|
374
380
|
provenance: ConfigProvenance;
|
|
375
381
|
}
|
|
376
|
-
/**
|
|
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
|
+
*/
|
|
377
387
|
export declare function overridesSavedProviderProfile(provenance: ConfigProvenance, hasEndpointOverride: boolean): boolean;
|
|
378
388
|
export declare const configHome: () => string;
|
|
379
389
|
export declare const stateHome: () => string;
|
|
@@ -402,6 +412,10 @@ export declare function setMcpServerEnabled(input: {
|
|
|
402
412
|
name: string;
|
|
403
413
|
enabled: boolean;
|
|
404
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>;
|
|
405
419
|
/** Atomically updates one project plugin override while preserving every unrelated JSON field. */
|
|
406
420
|
export declare function setProjectPluginEnabled(input: {
|
|
407
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. */
|
|
@@ -122,9 +129,14 @@ const configObjectSchema = z
|
|
|
122
129
|
auto: z.boolean().default(true),
|
|
123
130
|
threshold: z.number().min(0.1).max(0.99).default(0.85),
|
|
124
131
|
keepTurns: z.number().int().min(0).max(20).default(2),
|
|
132
|
+
/**
|
|
133
|
+
* Output token budget for the summarizer call. Larger than the agent-loop budget on
|
|
134
|
+
* purpose: a truncated summary is kept as partial, never re-run.
|
|
135
|
+
*/
|
|
136
|
+
maxOutputTokens: z.number().int().positive().default(16_000),
|
|
125
137
|
})
|
|
126
138
|
.strict()
|
|
127
|
-
.default(() => ({ auto: true, threshold: 0.85, keepTurns: 2 })),
|
|
139
|
+
.default(() => ({ auto: true, threshold: 0.85, keepTurns: 2, maxOutputTokens: 16_000 })),
|
|
128
140
|
websearch: z
|
|
129
141
|
.object({
|
|
130
142
|
provider: z
|
|
@@ -167,9 +179,24 @@ const configObjectSchema = z
|
|
|
167
179
|
});
|
|
168
180
|
export const configSchema = configObjectSchema.transform(({ mcpServers, ...config }) => ({
|
|
169
181
|
...config,
|
|
170
|
-
mcp: { servers: { ...config.mcp.servers, ...mcpServers } },
|
|
182
|
+
mcp: { ...config.mcp, servers: { ...config.mcp.servers, ...mcpServers } },
|
|
171
183
|
}));
|
|
172
|
-
/**
|
|
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
|
+
*/
|
|
173
200
|
export function overridesSavedProviderProfile(provenance, hasEndpointOverride) {
|
|
174
201
|
return hasEndpointOverride || !!provenance.selectedLayer?.hasLegacyProvider;
|
|
175
202
|
}
|
|
@@ -228,10 +255,12 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
|
228
255
|
global.config.skillOverrides = {};
|
|
229
256
|
let config = global.config;
|
|
230
257
|
let mcpSources = global.sources;
|
|
231
|
-
|
|
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 = "";
|
|
232
261
|
if (selectedFile && selectedFile !== globalFile) {
|
|
233
262
|
const selected = await parseLayer(selectedFile, options.file ? "explicit" : "project");
|
|
234
|
-
|
|
263
|
+
selectedLegacyModel = selected.config.provider.model;
|
|
235
264
|
const overlaid = { ...config };
|
|
236
265
|
for (const key of selected.keys) {
|
|
237
266
|
if (key === "mcp" || key === "mcpServers")
|
|
@@ -240,14 +269,17 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
|
240
269
|
overlaid[key] = selected.config[key];
|
|
241
270
|
}
|
|
242
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.
|
|
243
274
|
overlaid.mcp = {
|
|
275
|
+
...config.mcp,
|
|
244
276
|
servers: { ...config.mcp.servers, ...selected.config.mcp.servers },
|
|
245
277
|
};
|
|
246
278
|
mcpSources = { ...mcpSources, ...selected.sources };
|
|
247
279
|
config = overlaid;
|
|
248
280
|
}
|
|
249
281
|
else if (selectedFile) {
|
|
250
|
-
|
|
282
|
+
selectedLegacyModel = global.config.provider.model;
|
|
251
283
|
}
|
|
252
284
|
config.provider = providerSchema.parse({
|
|
253
285
|
...config.provider,
|
|
@@ -268,7 +300,7 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
|
268
300
|
? {
|
|
269
301
|
selectedLayer: {
|
|
270
302
|
kind: options.file ? "explicit" : "project",
|
|
271
|
-
hasLegacyProvider:
|
|
303
|
+
hasLegacyProvider: isUsableLegacyModel(selectedLegacyModel),
|
|
272
304
|
},
|
|
273
305
|
}
|
|
274
306
|
: {}),
|
|
@@ -321,6 +353,51 @@ export async function setMcpServerEnabled(input) {
|
|
|
321
353
|
}
|
|
322
354
|
return file;
|
|
323
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
|
+
}
|
|
324
401
|
/** Atomically updates one project plugin override while preserving every unrelated JSON field. */
|
|
325
402
|
export async function setProjectPluginEnabled(input) {
|
|
326
403
|
const file = join(input.workspace, ".alisio", "config.json");
|
|
@@ -2,6 +2,36 @@ import type { CompactionCheckpoint, CompletionRequest, Message, ModelProvider }
|
|
|
2
2
|
/** Rough provider-agnostic estimate: ~4 characters per token. */
|
|
3
3
|
export declare function estimateTokens(value: string | Message[]): number;
|
|
4
4
|
export declare function shouldCompact(used: number, window: number | undefined, threshold: number): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Declared context windows above this many tokens are treated as unknown for auto-compaction and
|
|
7
|
+
* for the context bar: a `window * threshold` trigger would dwarf the char-budget fallback and
|
|
8
|
+
* hide real pressure (e.g. an 8M-token window with a default 160k-char budget would never even
|
|
9
|
+
* reach 2% before running out of space). Documented in docs/compaction.md.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MAX_TRUSTED_WINDOW = 2000000;
|
|
12
|
+
export interface ContextBudget {
|
|
13
|
+
/** Effective total in tokens the context bar and auto-compaction measure against. */
|
|
14
|
+
total: number;
|
|
15
|
+
/** What the total is derived from: the model window, or the char-budget fallback. */
|
|
16
|
+
basis: "window" | "chars";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The single effective budget shared by the runner's auto-compaction and the TUI context bar.
|
|
20
|
+
* A known window (>= 1 token and <= MAX_TRUSTED_WINDOW) is used directly; otherwise the char
|
|
21
|
+
* budget is converted to estimated tokens (about 4 characters per token) so the bar fills where
|
|
22
|
+
* the char fallback actually compacts instead of showing an almost-empty bar next to an early
|
|
23
|
+
* compaction (the DeepSeek ~1M-window vs 160k-char budget mismatch).
|
|
24
|
+
*/
|
|
25
|
+
export declare function effectiveContextBudget(window: number | undefined, maxContextChars: number): ContextBudget;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to auto-compact before the next model call. Uses ONE effective budget: a known window
|
|
28
|
+
* triggers at `used >= window * threshold`; an unknown or guard-bounded window falls back to the
|
|
29
|
+
* char budget (`estimatedChars / 4 >= maxContextChars / 4`, keeping the old ~4-characters-per-token
|
|
30
|
+
* protection). `used` (provider-reported when available) feeds the window branch and the context
|
|
31
|
+
* bar; `estimatedChars` is the raw character count of the next request and feeds the fallback, so
|
|
32
|
+
* a provider's token report can never push an unknown-window session into needless compaction.
|
|
33
|
+
*/
|
|
34
|
+
export declare function shouldCompactContext(used: number, estimatedChars: number, window: number | undefined, maxContextChars: number, threshold: number): boolean;
|
|
5
35
|
export interface CompactionOptions {
|
|
6
36
|
/** Recent user turns kept verbatim. */
|
|
7
37
|
keepTurns?: number;
|
|
@@ -31,6 +61,11 @@ export interface CheckpointOutput {
|
|
|
31
61
|
text: string;
|
|
32
62
|
}
|
|
33
63
|
export declare function parseCheckpointOutput(raw: string, fields?: string[]): CheckpointOutput;
|
|
64
|
+
export interface TextCompletion {
|
|
65
|
+
text: string;
|
|
66
|
+
/** Provider reported an output-token cut (finish "length"): text may be partial. */
|
|
67
|
+
truncated: boolean;
|
|
68
|
+
}
|
|
34
69
|
export declare function summarize(provider: ModelProvider, request: {
|
|
35
70
|
instructions: string;
|
|
36
71
|
messages: Message[];
|
|
@@ -39,7 +74,7 @@ export declare function summarize(provider: ModelProvider, request: {
|
|
|
39
74
|
maxOutputTokens: number;
|
|
40
75
|
signal: AbortSignal;
|
|
41
76
|
sessionId?: string;
|
|
42
|
-
}): Promise<
|
|
77
|
+
}): Promise<TextCompletion>;
|
|
43
78
|
/** Narrow provider-agnostic completion used by compaction and the plugin model service. */
|
|
44
79
|
export declare function completeText(provider: ModelProvider, request: CompletionRequest & {
|
|
45
80
|
signal: AbortSignal;
|
package/dist/core/compaction.js
CHANGED
|
@@ -7,6 +7,39 @@ export function estimateTokens(value) {
|
|
|
7
7
|
export function shouldCompact(used, window, threshold) {
|
|
8
8
|
return !!window && window > 0 && used >= window * threshold;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Declared context windows above this many tokens are treated as unknown for auto-compaction and
|
|
12
|
+
* for the context bar: a `window * threshold` trigger would dwarf the char-budget fallback and
|
|
13
|
+
* hide real pressure (e.g. an 8M-token window with a default 160k-char budget would never even
|
|
14
|
+
* reach 2% before running out of space). Documented in docs/compaction.md.
|
|
15
|
+
*/
|
|
16
|
+
export const MAX_TRUSTED_WINDOW = 2_000_000;
|
|
17
|
+
/**
|
|
18
|
+
* The single effective budget shared by the runner's auto-compaction and the TUI context bar.
|
|
19
|
+
* A known window (>= 1 token and <= MAX_TRUSTED_WINDOW) is used directly; otherwise the char
|
|
20
|
+
* budget is converted to estimated tokens (about 4 characters per token) so the bar fills where
|
|
21
|
+
* the char fallback actually compacts instead of showing an almost-empty bar next to an early
|
|
22
|
+
* compaction (the DeepSeek ~1M-window vs 160k-char budget mismatch).
|
|
23
|
+
*/
|
|
24
|
+
export function effectiveContextBudget(window, maxContextChars) {
|
|
25
|
+
if (window !== undefined && window > 0 && window <= MAX_TRUSTED_WINDOW)
|
|
26
|
+
return { total: window, basis: "window" };
|
|
27
|
+
return { total: Math.max(1, Math.round(maxContextChars / 4)), basis: "chars" };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Whether to auto-compact before the next model call. Uses ONE effective budget: a known window
|
|
31
|
+
* triggers at `used >= window * threshold`; an unknown or guard-bounded window falls back to the
|
|
32
|
+
* char budget (`estimatedChars / 4 >= maxContextChars / 4`, keeping the old ~4-characters-per-token
|
|
33
|
+
* protection). `used` (provider-reported when available) feeds the window branch and the context
|
|
34
|
+
* bar; `estimatedChars` is the raw character count of the next request and feeds the fallback, so
|
|
35
|
+
* a provider's token report can never push an unknown-window session into needless compaction.
|
|
36
|
+
*/
|
|
37
|
+
export function shouldCompactContext(used, estimatedChars, window, maxContextChars, threshold) {
|
|
38
|
+
const budget = effectiveContextBudget(window, maxContextChars);
|
|
39
|
+
if (budget.basis === "window")
|
|
40
|
+
return used >= budget.total * threshold;
|
|
41
|
+
return estimatedChars / 4 >= budget.total;
|
|
42
|
+
}
|
|
10
43
|
/**
|
|
11
44
|
* Indexes where history can be split without separating an assistant tool call from its
|
|
12
45
|
* results: every call before the index is answered before the index, and the kept part
|
|
@@ -154,26 +187,41 @@ export async function summarize(provider, request) {
|
|
|
154
187
|
const focus = request.focus?.trim()
|
|
155
188
|
? `\n\nAdditional focus requested by the user: ${request.focus.trim()}`
|
|
156
189
|
: "";
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
let completion;
|
|
191
|
+
try {
|
|
192
|
+
completion = await completeTextWithMeta(provider, {
|
|
193
|
+
system: request.instructions,
|
|
194
|
+
messages: [
|
|
195
|
+
{
|
|
196
|
+
role: "user",
|
|
197
|
+
text: `Checkpoint this session transcript.${focus}\n\n<transcript>\n${serializeForSummary(request.messages)}\n</transcript>`,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
maxTokens: request.maxOutputTokens,
|
|
201
|
+
signal: request.signal,
|
|
202
|
+
...(request.model ? { model: request.model } : {}),
|
|
203
|
+
...(request.sessionId ? { sessionId: request.sessionId } : {}),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// The adapters report an empty "length" cut with a message about max output tokens.
|
|
208
|
+
if (!request.signal.aborted && String(error).includes("max output tokens"))
|
|
209
|
+
throw new Error("Compaction summary was cut off before any usable text was produced; raise compaction.maxOutputTokens");
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
if (completion.truncated && !completion.text)
|
|
213
|
+
throw new Error("Compaction summary was cut off before any usable text was produced; raise compaction.maxOutputTokens");
|
|
214
|
+
if (!completion.text)
|
|
171
215
|
throw new Error("Summarizer returned an empty summary");
|
|
172
|
-
return
|
|
216
|
+
return completion;
|
|
173
217
|
}
|
|
174
218
|
/** Narrow provider-agnostic completion used by compaction and the plugin model service. */
|
|
175
219
|
export async function completeText(provider, request) {
|
|
176
|
-
|
|
220
|
+
return (await completeTextWithMeta(provider, request)).text;
|
|
221
|
+
}
|
|
222
|
+
/** Like `completeText`, but also reports whether the provider cut the response by tokens. */
|
|
223
|
+
async function completeTextWithMeta(provider, request) {
|
|
224
|
+
let text = "", completed = false, truncated = false;
|
|
177
225
|
for await (const event of provider.stream({
|
|
178
226
|
instructions: request.system,
|
|
179
227
|
messages: request.messages.map((m) => m.role === "user"
|
|
@@ -188,6 +236,7 @@ export async function completeText(provider, request) {
|
|
|
188
236
|
request.signal.throwIfAborted();
|
|
189
237
|
if (event.type === "completed") {
|
|
190
238
|
completed = true;
|
|
239
|
+
truncated = event.message.truncated === true;
|
|
191
240
|
text = event.message.text || text;
|
|
192
241
|
}
|
|
193
242
|
else if (event.type === "text_delta")
|
|
@@ -195,7 +244,7 @@ export async function completeText(provider, request) {
|
|
|
195
244
|
}
|
|
196
245
|
if (!completed)
|
|
197
246
|
throw new Error("Completion ended without a completed response");
|
|
198
|
-
return text.trim();
|
|
247
|
+
return { text: text.trim(), truncated };
|
|
199
248
|
}
|
|
200
249
|
export function summaryMessage(body, replaced) {
|
|
201
250
|
return {
|