@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.
@@ -6,6 +6,11 @@ export interface CompactionSettings {
6
6
  auto?: boolean;
7
7
  threshold?: number;
8
8
  keepTurns?: number;
9
+ /**
10
+ * Per-call output token budget for the summarizer. Defaults to 4096 when unset; never falls
11
+ * back to the agent-loop `maxOutputTokens`. A truncated summary is accepted as partial.
12
+ */
13
+ maxOutputTokens?: number;
9
14
  }
10
15
  export interface RunnerOptions {
11
16
  /** Provider bound to this persisted session. */
@@ -23,7 +28,8 @@ export interface RunnerOptions {
23
28
  maxTokens?: number;
24
29
  onEvent?: (event: RunEvent) => void;
25
30
  readConcurrency?: number;
26
- /** Context window in tokens for a model, when known. Unknown disables auto compaction. */
31
+ /** Context window in tokens for a model, when known. Drives auto-compaction at `threshold`; when
32
+ * unknown (or absurdly large, see `MAX_TRUSTED_WINDOW`) the char budget falls back. */
27
33
  contextWindow?: (model: string) => number | undefined;
28
34
  compaction?: CompactionSettings;
29
35
  /** When set, write/process tools not allowed by the policy are offered and ask first. */
@@ -101,6 +107,19 @@ export declare class AgentRunner {
101
107
  private toolsText;
102
108
  /** Estimated tokens the next request of a session would send. */
103
109
  estimateContext(sessionId: string): Promise<number>;
110
+ /**
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.
117
+ */
118
+ contextBudget(model: string): {
119
+ total?: number;
120
+ basis: "window" | "unknown";
121
+ compactionAt: number;
122
+ };
104
123
  /** Change the model used by subsequent turns of a session; recorded in the store. */
105
124
  setModel(sessionId: string, model: string): void;
106
125
  /** Manually compact a session outside a run. */
@@ -1,5 +1,5 @@
1
1
  import { textResult, } from "@alisio/sdk";
2
- import { checkpointInstructions, estimateTokens, parseCheckpointOutput, planCompaction, shouldCompact, summarize, summaryMessage, } from "./compaction.js";
2
+ import { checkpointInstructions, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, 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.
@@ -111,6 +111,24 @@ export class AgentRunner {
111
111
  estimateTokens(this.options.store.messages(sessionId)) +
112
112
  estimateTokens(this.toolsText(this.availableTools())));
113
113
  }
114
+ /**
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.
121
+ */
122
+ contextBudget(model) {
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 };
131
+ }
114
132
  /** Change the model used by subsequent turns of a session; recorded in the store. */
115
133
  setModel(sessionId, model) {
116
134
  const id = model.trim();
@@ -183,12 +201,12 @@ export class AgentRunner {
183
201
  reportFailures(contribution.failures);
184
202
  const fields = Object.fromEntries(Object.entries(contribution.fields).map(([name, f]) => [name, f.description]));
185
203
  const provider = await (o.providerFor?.(o.store.get(sessionId)) ?? o.provider);
186
- const raw = await summarize(provider, {
204
+ const { text: raw, truncated: summaryTruncated } = await summarize(provider, {
187
205
  instructions: checkpointInstructions(contribution.instructions, fields),
188
206
  messages: plan.summarized,
189
207
  focus,
190
208
  model,
191
- maxOutputTokens: o.maxOutputTokens ?? 4096,
209
+ maxOutputTokens: o.compaction?.maxOutputTokens ?? 4096,
192
210
  signal,
193
211
  sessionId,
194
212
  });
@@ -230,6 +248,7 @@ export class AgentRunner {
230
248
  summarizedTokens: estimateTokens(plan.summarized),
231
249
  checkpointTokens: estimateTokens(summary.text),
232
250
  plugins: after.reports,
251
+ ...(summaryTruncated ? { partial: true } : {}),
233
252
  });
234
253
  return { replaced: plan.cut, before, after: afterTokens, summary };
235
254
  }
@@ -327,10 +346,12 @@ export class AgentRunner {
327
346
  ? last.tokens + estimateTokens(messages.slice(last.count))
328
347
  : estimateTokens(instructions) + estimateTokens(messages) + estimateTokens(toolsText);
329
348
  };
330
- const overChars = chars() > (o.maxContextChars ?? 160_000);
331
- const overWindow = compaction.auto !== false &&
332
- shouldCompact(used(), o.contextWindow?.(model), compaction.threshold ?? 0.85);
333
- if (overWindow || (overChars && compaction.auto !== false)) {
349
+ // One effective budget: a known window triggers at `used >= window * threshold`; an
350
+ // unknown (or absurdly large, see MAX_TRUSTED_WINDOW) window falls back to the char
351
+ // estimate (`chars / 4 >= maxContextChars / 4`). `maxContextChars` below stays as the
352
+ // post-compaction hard limit.
353
+ if (compaction.auto !== false &&
354
+ shouldCompactContext(used(), chars(), o.contextWindow?.(model), o.maxContextChars ?? 160_000, compaction.threshold ?? 0.85)) {
334
355
  await this.compactLocked(sessionId, emit, model, combined, "auto");
335
356
  instructions = await withPersona();
336
357
  messages = o.store.messages(sessionId);
@@ -338,6 +359,7 @@ export class AgentRunner {
338
359
  if (chars() > (o.maxContextChars ?? 160_000))
339
360
  throw new Error("Context budget exceeded and compaction could not reduce it; start a new session.");
340
361
  let completion;
362
+ let truncated = false;
341
363
  let usage;
342
364
  for await (const e of provider.stream({
343
365
  instructions,
@@ -358,6 +380,7 @@ export class AgentRunner {
358
380
  if (completion)
359
381
  throw new Error("Provider emitted multiple completions");
360
382
  completion = e.message;
383
+ truncated = e.message.truncated === true;
361
384
  usage = e.usage;
362
385
  tokens += (e.usage?.input ?? 0) + (e.usage?.output ?? 0);
363
386
  usageTotal.input += e.usage?.input ?? 0;
@@ -389,7 +412,19 @@ export class AgentRunner {
389
412
  ...(usage ? { usage } : {}),
390
413
  });
391
414
  if (!completion.calls.length) {
392
- emit("run_completed", { tokens, text: lastText });
415
+ const final = {
416
+ tokens,
417
+ text: lastText,
418
+ };
419
+ if (truncated) {
420
+ // The adapter kept a usable answer, but the token budget cut it off.
421
+ emit("response_truncated", {
422
+ turn: turn + 1,
423
+ maxOutputTokens: o.maxOutputTokens ?? 4096,
424
+ });
425
+ final.truncated = true;
426
+ }
427
+ emit("run_completed", final);
393
428
  return { sessionId, text: lastText, status: "completed", usage: usageTotal };
394
429
  }
395
430
  const prepared = completion.calls.map((call) => {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /** @alisio/core public API: embed the Alisio agent programmatically. */
2
2
  export * from "@alisio/sdk";
3
3
  export { type AppOptions, type BuiltinContext, type BuiltinPlugin, createApplication, enabledBuiltins, type PluginCatalogEntry, type PluginCatalogStatus, } from "./application.ts";
4
- export { type Config, type ConfigLoadResult, type ConfigProvenance, configFile, configHome, configSchema, type LoadedConfig, loadConfig, loadConfigWithProvenance, type McpServerSource, overridesSavedProviderProfile, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.ts";
5
- export { checkpointInstructions, completeText, estimateTokens, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, } from "./core/compaction.ts";
4
+ export { type Config, type ConfigLoadResult, type ConfigProvenance, configFile, configHome, configSchema, type LoadedConfig, loadConfig, loadConfigWithProvenance, type McpServerSource, overridesSavedProviderProfile, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.ts";
5
+ export { type ContextBudget, checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.ts";
6
6
  export type { ApprovalDecision, ApprovalHandler, ApprovalRequest, ContextSource, HookFailure, Policy, RunnerExtensions, Session, SessionStore, } from "./core/contracts.ts";
7
7
  export { ToolRegistry } from "./core/registry.ts";
8
8
  export { AgentRunner, type CompactionResult, defaultTokenBudget, type RunnerOptions, type RunOptions, } from "./core/runner.ts";
@@ -10,6 +10,7 @@ export { type ExtensionConflict, ExtensionRegistry } from "./extensions/registry
10
10
  export { HerdrBridge } from "./integrations/herdr.ts";
11
11
  export { McpConnector, type McpServerInfo, type McpStatus, type McpToolInfo, } from "./mcp/connector.ts";
12
12
  export { discoverPlugins, PluginHost, type PluginHostOptions, pluginPrefix, } from "./plugins/host.ts";
13
+ export { type CliInstallOptions, cliInstall, INSTALL_TIMEOUT_MS, type InstallPluginInput, type InstallPluginResult, type InstallRunner, installedNpmPlugins, installPlugin, npmInstallTarget, type PluginSpec, parsePluginSpec, pluginTrustRemark, printInstallResult, registerPluginInstallTool, sanitizeNpmError, } from "./plugins/install.ts";
13
14
  export { ActiveProvider, ProviderRegistry, UnconfiguredProvider } from "./providers/registry.ts";
14
15
  export { availableProviderModels, type ProviderModelCatalog, resolveProviderModel, } from "./providers/routing.ts";
15
16
  export { type ProviderProfile, type ProviderSettings, ProviderSettingsStore, } from "./providers/settings.ts";
@@ -19,7 +20,7 @@ export { type Skill, type SkillCatalogEntry, type SkillRoot, type SkillScope, Sk
19
20
  export { exists, fileSize, readHead, readJson, readText, which } from "./runtime/fs.ts";
20
21
  export { defaultGlobalRoots, isPathSpec, PLUGIN_KEYWORD, resolvePluginSpec, } from "./runtime/modules.ts";
21
22
  export { findWorkspace, safePath } from "./runtime/paths.ts";
22
- export { type ProcessResult, runProcess } from "./runtime/process.ts";
23
+ export { isMissingCommand, type ProcessResult, RIPGREP_INSTALL_HINT, runProcess, } from "./runtime/process.ts";
23
24
  export { isSqliteExperimentalWarning, openDatabase } from "./runtime/sqlite.ts";
24
25
  export { SQLiteStore } from "./runtime/store.ts";
25
26
  export { ChildSessions, type ChildSessionsOptions } from "./sessions/children.ts";
package/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  /** @alisio/core public API: embed the Alisio agent programmatically. */
2
2
  export * from "@alisio/sdk";
3
3
  export { createApplication, enabledBuiltins, } from "./application.js";
4
- export { configFile, configHome, configSchema, loadConfig, loadConfigWithProvenance, overridesSavedProviderProfile, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.js";
5
- export { checkpointInstructions, completeText, estimateTokens, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, } from "./core/compaction.js";
4
+ export { configFile, configHome, configSchema, loadConfig, loadConfigWithProvenance, overridesSavedProviderProfile, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.js";
5
+ export { checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.js";
6
6
  export { ToolRegistry } from "./core/registry.js";
7
7
  export { AgentRunner, defaultTokenBudget, } from "./core/runner.js";
8
8
  export { ExtensionRegistry } from "./extensions/registry.js";
9
9
  export { HerdrBridge } from "./integrations/herdr.js";
10
10
  export { McpConnector, } from "./mcp/connector.js";
11
11
  export { discoverPlugins, PluginHost, pluginPrefix, } from "./plugins/host.js";
12
+ export { cliInstall, INSTALL_TIMEOUT_MS, installedNpmPlugins, installPlugin, npmInstallTarget, parsePluginSpec, pluginTrustRemark, printInstallResult, registerPluginInstallTool, sanitizeNpmError, } from "./plugins/install.js";
12
13
  export { ActiveProvider, ProviderRegistry, UnconfiguredProvider } from "./providers/registry.js";
13
14
  export { availableProviderModels, resolveProviderModel, } from "./providers/routing.js";
14
15
  export { ProviderSettingsStore, } from "./providers/settings.js";
@@ -18,7 +19,7 @@ export { Skills, skillRoots, } from "./resources/skills.js";
18
19
  export { exists, fileSize, readHead, readJson, readText, which } from "./runtime/fs.js";
19
20
  export { defaultGlobalRoots, isPathSpec, PLUGIN_KEYWORD, resolvePluginSpec, } from "./runtime/modules.js";
20
21
  export { findWorkspace, safePath } from "./runtime/paths.js";
21
- export { runProcess } from "./runtime/process.js";
22
+ export { isMissingCommand, RIPGREP_INSTALL_HINT, runProcess, } from "./runtime/process.js";
22
23
  export { isSqliteExperimentalWarning, openDatabase } from "./runtime/sqlite.js";
23
24
  export { SQLiteStore } from "./runtime/store.js";
24
25
  export { ChildSessions } from "./sessions/children.js";
@@ -48,6 +48,8 @@ export declare class McpConnector {
48
48
  constructor(servers: Record<string, ServerConfig>, registry: ToolRegistry, workspace: string, availability?: "allowed" | "disabled" | "read-only", sources?: Record<string, McpServerSource>);
49
49
  register(): void;
50
50
  grantRuntimePermission(): void;
51
+ /** Drop the runtime grant: disconnect every server and unregister the bridge (used on revoke). */
52
+ revokeRuntimePermission(): Promise<void>;
51
53
  runtimePermission(): McpServerInfo["runtimePermission"];
52
54
  private assertAvailable;
53
55
  private client;
@@ -3,6 +3,7 @@ import { textResult } from "@alisio/sdk";
3
3
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
4
4
  import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
5
5
  import { objectSchema } from "../tools/standard.js";
6
+ import { loadVersion } from "../version.js";
6
7
  const TOOL_NAME_LIMIT = 64;
7
8
  const semanticSegment = (value, fallback) => value
8
9
  .normalize("NFKD")
@@ -138,6 +139,13 @@ export class McpConnector {
138
139
  this.availability = "allowed";
139
140
  this.register();
140
141
  }
142
+ /** Drop the runtime grant: disconnect every server and unregister the bridge (used on revoke). */
143
+ async revokeRuntimePermission() {
144
+ await Promise.all([...this.clients.keys()].map((name) => this.disconnect(name)));
145
+ for (const undo of this.bridgeUndo.splice(0).reverse())
146
+ undo();
147
+ this.availability = "disabled";
148
+ }
141
149
  runtimePermission() {
142
150
  return this.availability === "allowed"
143
151
  ? "granted"
@@ -212,7 +220,7 @@ export class McpConnector {
212
220
  }
213
221
  this.statuses.set(name, "connecting");
214
222
  this.diagnostics.delete(name);
215
- const client = new Client({ name: "alisio", version: "0.1.0-alpha.1" }, { capabilities: {} });
223
+ const client = new Client({ name: "alisio", version: loadVersion(import.meta.url) }, { capabilities: {} });
216
224
  const undo = [];
217
225
  const generation = Symbol(name);
218
226
  const transport = config.transport === "stdio"
@@ -566,7 +566,13 @@ export async function discoverPlugins(root) {
566
566
  throw e;
567
567
  }
568
568
  return entries
569
- .filter((e) => (e.isFile() && /\.[cm]?[jt]s$/.test(e.name)) || e.isDirectory())
569
+ .filter(
570
+ // A root may hold an `npm install --prefix` layout (`node_modules`, package.json/lock);
571
+ // npm packages there are resolved through the configured `plugins` array instead.
572
+ (e) => e.name !== "node_modules" &&
573
+ e.name !== "package.json" &&
574
+ e.name !== "package-lock.json" &&
575
+ ((e.isFile() && /\.[cm]?[jt]s$/.test(e.name)) || e.isDirectory()))
570
576
  .map((e) => join(root, e.name))
571
577
  .sort();
572
578
  }
@@ -0,0 +1,100 @@
1
+ import type { ToolRegistry } from "../core/registry.ts";
2
+ import { type ProcessResult } from "../runtime/process.ts";
3
+ export interface PluginSpec {
4
+ name: string;
5
+ version?: string;
6
+ }
7
+ /**
8
+ * Validates an install spec without touching the network: `npm:<package>[@<version>]` (canonical),
9
+ * a bare package name (same resolution), and nothing else. Rejects shell metacharacters, `..` /
10
+ * absolute paths, and unknown prefixes (`registry:`, `git:`, `file:`, URLs) with a clear error.
11
+ */
12
+ export declare function parsePluginSpec(spec: string): PluginSpec;
13
+ /** The npm argument for a spec: `name`, `name@<version>` or `name@latest` for `--update`. */
14
+ export declare function npmInstallTarget(name: string, version?: string, update?: boolean): string;
15
+ /**
16
+ * Injectable process runner so tests can substitute a fake `npm` without a network or a PATH shim.
17
+ * The default runs the real npm with a generous timeout and bounded output.
18
+ */
19
+ export type InstallRunner = (command: string, args: string[], options: {
20
+ cwd: string;
21
+ signal: AbortSignal;
22
+ env?: Record<string, string>;
23
+ }) => Promise<ProcessResult>;
24
+ export declare const INSTALL_TIMEOUT_MS = 300000;
25
+ export interface InstallPluginInput {
26
+ /** `npm:<package>[@<version>]` or a bare package name. */
27
+ spec: string;
28
+ /** Defaults to `configHome()` (ALISIO_CONFIG_HOME, else ~/.config/alisio). */
29
+ configHome?: string;
30
+ /** Refresh an already-installed package to `@latest`, keeping the npm name in the config. */
31
+ update?: boolean;
32
+ /** Refuse immediately; loading and installing executable plugins is disabled under --read-only. */
33
+ readOnly?: boolean;
34
+ signal?: AbortSignal;
35
+ /** Injected installer for tests (fake npm); defaults to the real npm. */
36
+ runner?: InstallRunner;
37
+ }
38
+ export interface InstallPluginResult {
39
+ packageName: string;
40
+ /** Version read back from the installed package.json, or "unknown". */
41
+ installedVersion: string;
42
+ /** `<configHome>/plugins/node_modules/<name>` (a directory). */
43
+ path: string;
44
+ /** The npm name persisted in the global configuration `plugins` array. */
45
+ configEntry: string;
46
+ /** The global configuration file that received the entry. */
47
+ configFile: string;
48
+ pluginsDir: string;
49
+ /** Already present in the global plugins directory before this call (no npm run performed). */
50
+ alreadyInstalled: boolean;
51
+ /** Refreshed to `@latest` by this call. */
52
+ updated: boolean;
53
+ /** Plain-text remark about where the plugin loads and how project trust applies. */
54
+ trustRemark: string;
55
+ }
56
+ /**
57
+ * Redacts probable secrets from npm output (auth tokens, JWTs, bearer credentials, userinfo in
58
+ * URLs) and caps the size, so a failing install never echoes credentials to the user.
59
+ */
60
+ export declare function sanitizeNpmError(text: string, maxLength?: number): string;
61
+ /** The shared install routine used by both the CLI command and the `plugin_install` tool. */
62
+ export declare function installPlugin(input: InstallPluginInput): Promise<InstallPluginResult>;
63
+ /** Final remark about loading and project trust, printed by the CLI and returned by the tool. */
64
+ export declare function pluginTrustRemark(name: string): string;
65
+ export interface CliInstallOptions {
66
+ spec: string;
67
+ configHome?: string;
68
+ /** `--yes`/`--trust-plugin`: skip the interactive pre-install confirmation. */
69
+ yes?: boolean;
70
+ update?: boolean;
71
+ readOnly?: boolean;
72
+ /** True when the CLI can prompt (interactive TTY, not --json). */
73
+ interactive?: boolean;
74
+ /** Print the result as JSON instead of human lines. */
75
+ json?: boolean;
76
+ signal?: AbortSignal;
77
+ runner?: InstallRunner;
78
+ /** Injectable confirmation for the interactive path (tests); defaults to a readline prompt. */
79
+ confirm?: () => Promise<boolean>;
80
+ }
81
+ export declare function cliInstall(options: CliInstallOptions): Promise<InstallPluginResult>;
82
+ export declare function printInstallResult(result: InstallPluginResult, options?: {
83
+ json?: boolean;
84
+ }): void;
85
+ /**
86
+ * Registers the host-owned `plugin_install` tool (agent capability). It has process effect so it
87
+ * goes through the existing write/process/external permission gate (TUI approval or
88
+ * `--allow-process`; never available under `--read-only`, where it is not registered at all).
89
+ */
90
+ export declare function registerPluginInstallTool(registry: ToolRegistry, options?: {
91
+ readOnly?: boolean;
92
+ configHome?: string;
93
+ runner?: InstallRunner;
94
+ }): void;
95
+ /**
96
+ * Lists npm packages installed under `<root>/node_modules` that declare the `alisio-plugin`
97
+ * keyword — the same packages `plugins`-array entries resolve to. Used by `alisio plugins list`
98
+ * so an installed package shows up alongside file/directory plugins.
99
+ */
100
+ export declare function installedNpmPlugins(root: string): Promise<string[]>;