@alisio/core 0.1.0-alpha.9 → 0.1.1

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.
Files changed (187) hide show
  1. package/README.md +52 -7
  2. package/dist/agents/active.d.ts +99 -0
  3. package/dist/agents/active.js +218 -0
  4. package/dist/agents/capabilities.d.ts +26 -0
  5. package/dist/agents/capabilities.js +120 -0
  6. package/dist/agents/create-agent-skill.d.ts +11 -0
  7. package/dist/agents/create-agent-skill.js +60 -0
  8. package/dist/agents/definitions.d.ts +88 -0
  9. package/dist/agents/definitions.js +404 -0
  10. package/dist/agents/draft.d.ts +41 -0
  11. package/dist/agents/draft.js +133 -0
  12. package/dist/agents/service.d.ts +57 -0
  13. package/dist/agents/service.js +86 -0
  14. package/dist/agents/templates.d.ts +8 -0
  15. package/dist/agents/templates.js +102 -0
  16. package/dist/analysis/capabilities.d.ts +48 -0
  17. package/dist/analysis/capabilities.js +90 -0
  18. package/dist/analysis/data/csv.d.ts +46 -0
  19. package/dist/analysis/data/csv.js +240 -0
  20. package/dist/analysis/data/datasets.d.ts +132 -0
  21. package/dist/analysis/data/datasets.js +441 -0
  22. package/dist/analysis/data/describe.d.ts +27 -0
  23. package/dist/analysis/data/describe.js +103 -0
  24. package/dist/analysis/data/engine-client.d.ts +58 -0
  25. package/dist/analysis/data/engine-client.js +304 -0
  26. package/dist/analysis/data/engine-entry.d.ts +1 -0
  27. package/dist/analysis/data/engine-entry.js +59 -0
  28. package/dist/analysis/data/engine-source.d.ts +4 -0
  29. package/dist/analysis/data/engine-source.js +6 -0
  30. package/dist/analysis/data/engine-types.d.ts +86 -0
  31. package/dist/analysis/data/engine-types.js +7 -0
  32. package/dist/analysis/data/infer.d.ts +42 -0
  33. package/dist/analysis/data/infer.js +118 -0
  34. package/dist/analysis/data/ingest-worker.d.ts +17 -0
  35. package/dist/analysis/data/ingest-worker.js +365 -0
  36. package/dist/analysis/data/json.d.ts +19 -0
  37. package/dist/analysis/data/json.js +44 -0
  38. package/dist/analysis/data/query-worker.d.ts +12 -0
  39. package/dist/analysis/data/query-worker.js +75 -0
  40. package/dist/analysis/data/rows.d.ts +39 -0
  41. package/dist/analysis/data/rows.js +120 -0
  42. package/dist/analysis/data/sql-guard.d.ts +20 -0
  43. package/dist/analysis/data/sql-guard.js +129 -0
  44. package/dist/analysis/data/stats.d.ts +18 -0
  45. package/dist/analysis/data/stats.js +77 -0
  46. package/dist/analysis/install-hints.d.ts +33 -0
  47. package/dist/analysis/install-hints.js +144 -0
  48. package/dist/analysis/janitor.d.ts +60 -0
  49. package/dist/analysis/janitor.js +264 -0
  50. package/dist/analysis/jobs.d.ts +94 -0
  51. package/dist/analysis/jobs.js +165 -0
  52. package/dist/analysis/oci.d.ts +129 -0
  53. package/dist/analysis/oci.js +330 -0
  54. package/dist/analysis/python/sources.d.ts +2 -0
  55. package/dist/analysis/python/sources.js +10 -0
  56. package/dist/analysis/requirements.d.ts +32 -0
  57. package/dist/analysis/requirements.js +73 -0
  58. package/dist/analysis/rerun.d.ts +40 -0
  59. package/dist/analysis/rerun.js +136 -0
  60. package/dist/analysis/runtime-manager.d.ts +146 -0
  61. package/dist/analysis/runtime-manager.js +472 -0
  62. package/dist/analysis/status.d.ts +21 -0
  63. package/dist/analysis/status.js +52 -0
  64. package/dist/application.d.ts +105 -3
  65. package/dist/application.js +446 -18
  66. package/dist/artifacts/kinds.d.ts +35 -0
  67. package/dist/artifacts/kinds.js +177 -0
  68. package/dist/artifacts/publisher.d.ts +7 -0
  69. package/dist/artifacts/publisher.js +29 -0
  70. package/dist/artifacts/store.d.ts +119 -0
  71. package/dist/artifacts/store.js +581 -0
  72. package/dist/background/janitor.d.ts +22 -0
  73. package/dist/background/janitor.js +62 -0
  74. package/dist/background/mirror.d.ts +14 -0
  75. package/dist/background/mirror.js +41 -0
  76. package/dist/background/notify.d.ts +58 -0
  77. package/dist/background/notify.js +172 -0
  78. package/dist/background/output.d.ts +46 -0
  79. package/dist/background/output.js +179 -0
  80. package/dist/background/service.d.ts +137 -0
  81. package/dist/background/service.js +410 -0
  82. package/dist/background/store.d.ts +103 -0
  83. package/dist/background/store.js +195 -0
  84. package/dist/changelog/data.d.ts +6 -0
  85. package/dist/changelog/data.js +306 -0
  86. package/dist/changelog/index.d.ts +5 -0
  87. package/dist/changelog/index.js +7 -0
  88. package/dist/changelog/parse.d.ts +53 -0
  89. package/dist/changelog/parse.js +160 -0
  90. package/dist/commands/builtins.d.ts +10 -0
  91. package/dist/commands/builtins.js +161 -0
  92. package/dist/commands/catalog.d.ts +86 -0
  93. package/dist/commands/catalog.js +304 -0
  94. package/dist/config.d.ts +206 -6
  95. package/dist/config.js +426 -20
  96. package/dist/core/compaction.js +13 -2
  97. package/dist/core/contracts.d.ts +180 -3
  98. package/dist/core/opt-in.d.ts +12 -0
  99. package/dist/core/opt-in.js +12 -0
  100. package/dist/core/output-limit.d.ts +34 -0
  101. package/dist/core/output-limit.js +39 -0
  102. package/dist/core/run-stats.d.ts +16 -0
  103. package/dist/core/run-stats.js +8 -0
  104. package/dist/core/runner.d.ts +134 -4
  105. package/dist/core/runner.js +780 -138
  106. package/dist/core/timeout.d.ts +15 -0
  107. package/dist/core/timeout.js +61 -0
  108. package/dist/core/truncation.d.ts +30 -0
  109. package/dist/core/truncation.js +83 -0
  110. package/dist/goal/blockers.d.ts +45 -0
  111. package/dist/goal/blockers.js +51 -0
  112. package/dist/goal/budget.d.ts +24 -0
  113. package/dist/goal/budget.js +53 -0
  114. package/dist/goal/command.d.ts +36 -0
  115. package/dist/goal/command.js +98 -0
  116. package/dist/goal/format.d.ts +19 -0
  117. package/dist/goal/format.js +102 -0
  118. package/dist/goal/host.d.ts +29 -0
  119. package/dist/goal/host.js +42 -0
  120. package/dist/goal/index.d.ts +9 -0
  121. package/dist/goal/index.js +9 -0
  122. package/dist/goal/machine.d.ts +186 -0
  123. package/dist/goal/machine.js +344 -0
  124. package/dist/goal/prompts.d.ts +10 -0
  125. package/dist/goal/prompts.js +75 -0
  126. package/dist/goal/service.d.ts +113 -0
  127. package/dist/goal/service.js +273 -0
  128. package/dist/goal/store.d.ts +59 -0
  129. package/dist/goal/store.js +184 -0
  130. package/dist/index.d.ts +50 -5
  131. package/dist/index.js +49 -4
  132. package/dist/mcp/connector.js +38 -6
  133. package/dist/mcp/rich.d.ts +41 -0
  134. package/dist/mcp/rich.js +362 -0
  135. package/dist/permissions/modes.d.ts +72 -0
  136. package/dist/permissions/modes.js +98 -0
  137. package/dist/plan/exit-plan.d.ts +25 -0
  138. package/dist/plan/exit-plan.js +130 -0
  139. package/dist/plan/state.d.ts +102 -0
  140. package/dist/plan/state.js +165 -0
  141. package/dist/plugins/host.d.ts +34 -1
  142. package/dist/plugins/host.js +143 -9
  143. package/dist/providers/settings.d.ts +15 -0
  144. package/dist/providers/settings.js +38 -0
  145. package/dist/reload.d.ts +75 -0
  146. package/dist/reload.js +204 -0
  147. package/dist/resources/context.js +6 -1
  148. package/dist/runtime/access.d.ts +62 -0
  149. package/dist/runtime/access.js +115 -0
  150. package/dist/runtime/blobs.d.ts +51 -0
  151. package/dist/runtime/blobs.js +177 -0
  152. package/dist/runtime/diff.d.ts +23 -0
  153. package/dist/runtime/diff.js +154 -0
  154. package/dist/runtime/ids.d.ts +8 -0
  155. package/dist/runtime/ids.js +32 -0
  156. package/dist/runtime/paths.d.ts +11 -0
  157. package/dist/runtime/paths.js +17 -1
  158. package/dist/runtime/process.d.ts +22 -0
  159. package/dist/runtime/process.js +100 -15
  160. package/dist/runtime/sqlite.d.ts +2 -0
  161. package/dist/runtime/sqlite.js +18 -0
  162. package/dist/runtime/store.d.ts +91 -3
  163. package/dist/runtime/store.js +465 -11
  164. package/dist/runtime/zip.d.ts +24 -0
  165. package/dist/runtime/zip.js +126 -0
  166. package/dist/sessions/children.js +17 -1
  167. package/dist/sessions/side-questions.d.ts +57 -0
  168. package/dist/sessions/side-questions.js +161 -0
  169. package/dist/tools/analysis.d.ts +37 -0
  170. package/dist/tools/analysis.js +573 -0
  171. package/dist/tools/artifacts.d.ts +14 -0
  172. package/dist/tools/artifacts.js +143 -0
  173. package/dist/tools/background.d.ts +7 -0
  174. package/dist/tools/background.js +165 -0
  175. package/dist/tools/data.d.ts +7 -0
  176. package/dist/tools/data.js +160 -0
  177. package/dist/tools/execute.d.ts +5 -0
  178. package/dist/tools/execute.js +15 -3
  179. package/dist/tools/goal.d.ts +19 -0
  180. package/dist/tools/goal.js +126 -0
  181. package/dist/tools/search.d.ts +17 -3
  182. package/dist/tools/search.js +153 -0
  183. package/dist/tools/standard.d.ts +4 -1
  184. package/dist/tools/standard.js +99 -18
  185. package/dist/version.d.ts +5 -3
  186. package/dist/version.js +35 -6
  187. package/package.json +2 -2
@@ -1,9 +1,26 @@
1
+ import { realpath } from "node:fs/promises";
1
2
  import { homedir } from "node:os";
2
3
  import { dirname, join, resolve } from "node:path";
4
+ import { AgentDefinitionService } from "./agents/service.js";
5
+ import { CapabilityGrants } from "./analysis/capabilities.js";
6
+ import { DatasetService } from "./analysis/data/datasets.js";
7
+ import { AnalysisJanitor } from "./analysis/janitor.js";
8
+ import { AnalysisJobs } from "./analysis/jobs.js";
9
+ import { OciRuntime } from "./analysis/oci.js";
10
+ import { AnalysisRerun } from "./analysis/rerun.js";
11
+ import { AnalysisRuntimeManager } from "./analysis/runtime-manager.js";
12
+ import { createArtifactPublisher } from "./artifacts/publisher.js";
13
+ import { ArtifactStore } from "./artifacts/store.js";
14
+ import { TaskJanitor } from "./background/janitor.js";
15
+ import { subagentTasks } from "./background/mirror.js";
16
+ import { BackgroundTasks } from "./background/service.js";
3
17
  import { configFile, configHome, loadConfigWithProvenance, overridesSavedProviderProfile, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.js";
4
18
  import { completeText } from "./core/compaction.js";
19
+ import { DEFAULT_MAX_OUTPUT_TOKENS } from "./core/output-limit.js";
5
20
  import { ToolRegistry } from "./core/registry.js";
6
21
  import { AgentRunner } from "./core/runner.js";
22
+ import { GoalService } from "./goal/service.js";
23
+ import { GoalStore } from "./goal/store.js";
7
24
  import { HerdrBridge } from "./integrations/herdr.js";
8
25
  import { McpConnector } from "./mcp/connector.js";
9
26
  import { discoverPlugins, PluginHost } from "./plugins/host.js";
@@ -13,10 +30,21 @@ import { ProviderSettingsStore } from "./providers/settings.js";
13
30
  import { ProjectContext } from "./resources/context.js";
14
31
  import { expandSlashPrompt, loadPromptTemplates, promptSources } from "./resources/prompts.js";
15
32
  import { Skills, skillRoots } from "./resources/skills.js";
33
+ import { PathAccess } from "./runtime/access.js";
34
+ import { BlobStore } from "./runtime/blobs.js";
16
35
  import { isPathSpec } from "./runtime/modules.js";
17
36
  import { findWorkspace } from "./runtime/paths.js";
18
37
  import { SQLiteStore } from "./runtime/store.js";
19
38
  import { ChildSessions } from "./sessions/children.js";
39
+ import { SideQuestions } from "./sessions/side-questions.js";
40
+ import { registerGoalTools } from "./tools/goal.js";
41
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
42
+ /**
43
+ * Cap per teardown stage of `app.close()`: exit must never wait longer than this on a hanging
44
+ * component (MCP server, provider, plugin). Stages run in parallel where independent and
45
+ * failures are swallowed, so teardown errors never replace the caller's result.
46
+ */
47
+ const TEARDOWN_STAGE_TIMEOUT_MS = 2_500;
20
48
  /** A built-in runs unless its config sets `enabled: false` or it is disabled by id. */
21
49
  export function enabledBuiltins(available, config, disabled = []) {
22
50
  for (const id of [...Object.keys(config), ...disabled])
@@ -52,7 +80,28 @@ export async function createApplication(options = {}) {
52
80
  // a project value (see loadConfigWithProvenance), so a project cannot grant itself MCP consent.
53
81
  const mcpAllow = config.mcp.allow === true;
54
82
  let persistedMcpAllow = mcpAllow;
83
+ // Extra tool roots: config `additionalDirectories` (already canonicalized on load) plus per-run
84
+ // `--add-dir` (resolved against the current directory). Ignored entirely under --read-only, so
85
+ // the flag and the config key can never widen a locked session.
86
+ const addDirs = options.readOnly
87
+ ? []
88
+ : await Promise.all((options.addDirs ?? []).map((dir) => realpath(resolve(cwd, dir)).catch(() => resolve(cwd, dir))));
89
+ const pathAccess = new PathAccess({
90
+ workspace,
91
+ extraRoots: [...config.additionalDirectories, ...addDirs],
92
+ ...(options.approveExternalDirectory && !options.readOnly
93
+ ? { approve: options.approveExternalDirectory }
94
+ : {}),
95
+ readOnly: !!options.readOnly,
96
+ });
55
97
  const store = new SQLiteStore(options.db ?? join(stateHome(), "sessions.sqlite"));
98
+ // Content-addressed attachment bytes next to the session database (no I/O until `put`).
99
+ const blobs = new BlobStore({
100
+ root: options.db && options.db !== ":memory:"
101
+ ? join(dirname(options.db), "blobs")
102
+ : join(stateHome(), "blobs"),
103
+ db: store.db,
104
+ });
56
105
  const registry = new ToolRegistry(), skills = new Skills({ overrides: config.skillOverrides }), context = new ProjectContext(workspace, {
57
106
  globalDir: configHome(),
58
107
  cwd,
@@ -234,7 +283,150 @@ export async function createApplication(options = {}) {
234
283
  }, {
235
284
  config: config.websearch,
236
285
  resolveExtension: () => plugins.extensions.resolve("websearch"),
286
+ }, pathAccess);
287
+ // Python analysis and artifacts live next to the session database (like attachment blobs).
288
+ const stateRoot = options.db && options.db !== ":memory:" ? dirname(options.db) : stateHome();
289
+ const artifacts = new ArtifactStore({
290
+ root: stateRoot,
291
+ db: store.db,
292
+ limits: {
293
+ maxFiles: config.analysis.limits.maxFiles,
294
+ maxFileBytes: config.analysis.limits.maxFileBytes,
295
+ maxOutputBytes: config.analysis.limits.maxOutputBytes,
296
+ },
297
+ });
298
+ const capabilityGrants = new CapabilityGrants({
299
+ db: store.db,
300
+ rootOf: (id) => store.rootOf(id),
301
+ });
302
+ const analysisRuntime = options.analysisRuntime ??
303
+ new AnalysisRuntimeManager({
304
+ stateDir: stateRoot,
305
+ ...(options.python ? { python: options.python } : {}),
306
+ });
307
+ const analysisJobs = new AnalysisJobs({ root: stateRoot, db: store.db });
308
+ // Container mode (`analysis.runtime: "oci"`): read from the live config on every call.
309
+ let ociRuntime;
310
+ const oci = {
311
+ mode: () => config.analysis.runtime,
312
+ runtime: () => {
313
+ const key = JSON.stringify(config.analysis.oci);
314
+ if (ociRuntime?.key !== key)
315
+ ociRuntime = {
316
+ key,
317
+ runtime: new OciRuntime({ ...config.analysis.oci }, options.analysisOci),
318
+ };
319
+ return ociRuntime.runtime;
320
+ },
321
+ };
322
+ // Tabular datasets (one SQLite file each); XLSX needs the discovered Python.
323
+ const datasets = new DatasetService({
324
+ root: stateRoot,
325
+ db: store.db,
326
+ limits: config.analysis.data,
327
+ python: analysisRuntime,
237
328
  });
329
+ const analysisRerun = new AnalysisRerun({ jobs: analysisJobs, store: artifacts, datasets });
330
+ // Retention: a sweep at most every 24 h, launched in the background after the start.
331
+ const janitor = new AnalysisJanitor({
332
+ root: stateRoot,
333
+ db: store.db,
334
+ artifacts,
335
+ datasets,
336
+ blobs,
337
+ retention: () => config.analysis.retention,
338
+ ...(options.analysisSweepDelayMs !== undefined
339
+ ? { initialDelayMs: options.analysisSweepDelayMs }
340
+ : {}),
341
+ });
342
+ // Background tasks: a service over the shared database (state change = compare-and-set), logs
343
+ // under `<state>/tasks/`. Tasks of a dead owner process (a previous run) become `lost` now.
344
+ const tasks = new BackgroundTasks({
345
+ db: store.db,
346
+ stateRoot,
347
+ rootOf: (id) => store.rootOf(id),
348
+ limits: () => config.tasks,
349
+ readOnly: !!options.readOnly,
350
+ ...(options.tasks?.wake ? { wake: options.tasks.wake } : {}),
351
+ ...(options.tasks?.onChange ? { onChange: options.tasks.onChange } : {}),
352
+ ...(options.tasks?.timing ? { timing: options.tasks.timing } : {}),
353
+ ...(options.tasks?.maxPerProcess ? { maxPerProcess: options.tasks.maxPerProcess } : {}),
354
+ ...(options.tasks?.killGraceMs ? { killGraceMs: options.tasks.killGraceMs } : {}),
355
+ mirror: (root) => subagentTasks(plugins.panels, root),
356
+ onError: (error) => process.stderr.write(`Background task error: ${error instanceof Error ? error.message : String(error)}\n`),
357
+ });
358
+ tasks.recover();
359
+ const taskJanitor = new TaskJanitor({
360
+ store: tasks.store,
361
+ stateRoot,
362
+ retentionDays: () => config.tasks.retentionDays,
363
+ ...(options.tasks?.sweepDelayMs !== undefined
364
+ ? { initialDelayMs: options.tasks.sweepDelayMs }
365
+ : {}),
366
+ });
367
+ if (config.tasks.enabled) {
368
+ const { registerBackgroundTools } = await import("./tools/background.js");
369
+ registerBackgroundTools(registry, { tasks, rootOf: (id) => store.rootOf(id) });
370
+ }
371
+ // Session goals (`/goal`): one service over the shared database. A goal left `active` by a
372
+ // process that is gone pauses with reason `restart` now (never auto-resumed). The two goal
373
+ // tools are opt-in: a run only sees them while its session has an active goal.
374
+ const goals = new GoalService({
375
+ store: new GoalStore(store.db),
376
+ settings: () => config.goal,
377
+ ...(options.goals?.onChange ? { onChange: options.goals.onChange } : {}),
378
+ ...(options.goals?.onLog ? { onLog: options.goals.onLog } : {}),
379
+ });
380
+ if (options.goals?.recover !== false)
381
+ goals.recover();
382
+ registerGoalTools(registry, { goals });
383
+ // R4: nothing is registered with analysis disabled or under --read-only. Startup only looks
384
+ // for candidate interpreters with `stat`; no process is launched until the first call.
385
+ const analysisEnabled = config.analysis.enabled && !options.readOnly;
386
+ if (analysisEnabled) {
387
+ const [candidates, active] = await Promise.all([
388
+ analysisRuntime.candidates(),
389
+ analysisRuntime.activeRuntime(),
390
+ ]);
391
+ const { registerAnalysisTools } = await import("./tools/analysis.js");
392
+ registerAnalysisTools(registry, {
393
+ store: artifacts,
394
+ jobs: analysisJobs,
395
+ runtime: analysisRuntime,
396
+ datasets,
397
+ oci,
398
+ rerun: analysisRerun,
399
+ rootOf: (id) => store.rootOf(id),
400
+ // Read live: a changed `analysis.limits.*` setting applies to the next call.
401
+ limits: {
402
+ get timeoutMs() {
403
+ return config.analysis.limits.timeoutMs;
404
+ },
405
+ get maxLogBytes() {
406
+ return config.analysis.limits.maxLogBytes;
407
+ },
408
+ },
409
+ startup: { candidates: candidates.length > 0, extras: active?.extras ?? [] },
410
+ });
411
+ const { registerArtifactTools } = await import("./tools/artifacts.js");
412
+ registerArtifactTools(registry, { store: artifacts, rootOf: (id) => store.rootOf(id) });
413
+ }
414
+ // `exit_plan` (opt-in: only the plan agent's run sees it): the plan review hand-over.
415
+ const { registerExitPlan } = await import("./plan/exit-plan.js");
416
+ registerExitPlan(registry, { store, ui: plugins.ui, readOnly: !!options.readOnly });
417
+ // Data tools only read (effect `read`): they stay available under --read-only.
418
+ if (config.analysis.enabled) {
419
+ const { registerDataTools } = await import("./tools/data.js");
420
+ registerDataTools(registry, { datasets, rootOf: (id) => store.rootOf(id) });
421
+ }
422
+ const sessionWorkspace = (sessionId) => {
423
+ try {
424
+ return store.get(sessionId).workspace;
425
+ }
426
+ catch {
427
+ return workspace;
428
+ }
429
+ };
238
430
  const { registerPluginInstallTool } = await import("./plugins/install.js");
239
431
  registerPluginInstallTool(registry, { readOnly: !!options.readOnly });
240
432
  if ((options.allowMcp || mcpAllow) && !options.readOnly)
@@ -475,6 +667,7 @@ export async function createApplication(options = {}) {
475
667
  const runtimePolicy = {
476
668
  write: !!options.allowWrite && !options.readOnly,
477
669
  process: !!options.allowProcess && !options.readOnly,
670
+ ...(options.allowAnalysis && !options.readOnly ? { analysis: true } : {}),
478
671
  external: !options.readOnly &&
479
672
  (!!options.allowExternal ||
480
673
  !!options.allowMcp ||
@@ -483,6 +676,52 @@ export async function createApplication(options = {}) {
483
676
  plugins.externalCount > 0 ||
484
677
  registry.list().some((t) => t.name.startsWith("p_"))),
485
678
  };
679
+ /** Model and provider of a session, for the provenance of its artifacts. */
680
+ const provenanceOf = (sessionId) => {
681
+ try {
682
+ const session = store.get(sessionId);
683
+ return {
684
+ ...(session.model ? { model: session.model } : {}),
685
+ ...(session.provider ? { provider: session.provider } : {}),
686
+ };
687
+ }
688
+ catch {
689
+ return {};
690
+ }
691
+ };
692
+ /**
693
+ * The maximum output a model's catalog declares (`ModelInfo.maxOutputTokens`), for the budget
694
+ * of each request. Looks in the active provider's catalog first (loading it once, bounded, if
695
+ * the first request comes before it); then in the catalogs of the configured profiles that
696
+ * were already listed, taking the smallest declaration when several providers list the same
697
+ * model id so a derived budget never exceeds any of them. Unknown stays unknown.
698
+ */
699
+ const declaredOutputLimit = async (model) => {
700
+ if (!models.has(model)) {
701
+ const loading = ensureModels();
702
+ if (loading) {
703
+ let timer;
704
+ await Promise.race([
705
+ loading.catch(() => undefined),
706
+ new Promise((resolve) => {
707
+ timer = setTimeout(resolve, 2_000);
708
+ }),
709
+ ]);
710
+ if (timer)
711
+ clearTimeout(timer);
712
+ }
713
+ }
714
+ const own = models.get(model)?.maxOutputTokens;
715
+ if (own !== undefined)
716
+ return own;
717
+ const declared = catalogCache
718
+ ? availableProviderModels(catalogCache.value)
719
+ .filter((entry) => entry.model.id === model)
720
+ .map((entry) => entry.model.maxOutputTokens)
721
+ .filter((value) => typeof value === "number" && value > 0)
722
+ : [];
723
+ return declared.length ? Math.min(...declared) : undefined;
724
+ };
486
725
  const runner = new AgentRunner({
487
726
  provider,
488
727
  providerFor,
@@ -495,7 +734,43 @@ export async function createApplication(options = {}) {
495
734
  compaction: config.compaction,
496
735
  extensions: plugins,
497
736
  contextWindow,
737
+ modelOutputLimit: declaredOutputLimit,
738
+ fallbackMaxOutputTokens: DEFAULT_MAX_OUTPUT_TOKENS,
739
+ effortLevels: (model) => {
740
+ const known = models.get(model)?.effort;
741
+ if (!known)
742
+ void ensureModels();
743
+ return known;
744
+ },
498
745
  ...(options.approve && !options.readOnly ? { approve: options.approve } : {}),
746
+ pathAccess,
747
+ ...(analysisEnabled
748
+ ? {
749
+ capabilities: {
750
+ granted: (capability, sessionId) => capabilityGrants.granted(capability, sessionId),
751
+ record: (input) => {
752
+ capabilityGrants.record({ ...input, workspace: sessionWorkspace(input.sessionId) });
753
+ },
754
+ source: options.approvalSource ?? "tui",
755
+ get runtime() {
756
+ return config.analysis.runtime;
757
+ },
758
+ // A rerun's approval shows the saved script (the input carries only its reference).
759
+ preview: async (input, sessionId) => typeof input.rerunOf === "string"
760
+ ? analysisRerun.script(input.rerunOf, store.rootOf(sessionId))
761
+ : undefined,
762
+ },
763
+ }
764
+ : {}),
765
+ artifacts: (call, announce) => createArtifactPublisher(artifacts, {
766
+ sessionId: call.sessionId,
767
+ rootSessionId: store.rootOf(call.sessionId),
768
+ workspace: sessionWorkspace(call.sessionId),
769
+ runId: call.runId,
770
+ callId: call.callId,
771
+ // Public provenance of what produced the artifact (no paths, no keys).
772
+ provenance: provenanceOf(call.sessionId),
773
+ }, announce),
499
774
  ...(config.websearch.provider === "native"
500
775
  ? { nativeTools: [{ type: config.websearch.nativeToolType }] }
501
776
  : {}),
@@ -508,6 +783,10 @@ export async function createApplication(options = {}) {
508
783
  // Child sessions (generic delegation service for plugins). Contexts are cached per workspace
509
784
  // so a child in a git worktree reads that worktree's AGENTS.md files.
510
785
  const contexts = new Map([[workspace, context]]);
786
+ // Startup reconciliation: runs left queued/running by a dead process become `interrupted`
787
+ // (runs of this process, e.g. another Application in a server, are kept). Child sessions are
788
+ // reconciled by `interruptStale()` in the ChildSessions constructor right below.
789
+ store.interruptRuns();
511
790
  plugins.setSessions(new ChildSessions({
512
791
  store,
513
792
  runner,
@@ -574,12 +853,43 @@ export async function createApplication(options = {}) {
574
853
  }
575
854
  }
576
855
  }
856
+ // Retention runs in the background (unreferenced timer); never for in-memory test databases.
857
+ if (analysisEnabled && options.db !== ":memory:")
858
+ janitor.start();
859
+ // Retention is maintenance that deletes files: never under --read-only, never for `:memory:`.
860
+ if (options.db !== ":memory:" && !options.readOnly)
861
+ taskJanitor.start();
577
862
  return {
578
863
  workspace,
864
+ pathAccess,
579
865
  config,
580
866
  prompts,
581
867
  expandPrompt,
582
868
  store,
869
+ /** Uploaded attachment bytes; resolve a `BlobRef` with `blobs.attachment(ref)` before a run. */
870
+ blobs,
871
+ /** Published artifacts of every session (folders under `<state>/artifacts`). */
872
+ artifacts,
873
+ /** Persisted capability grants (`analysis.run`) and their audit rows. */
874
+ capabilityGrants,
875
+ /** Background tasks (`bg_*` tools, `/tasks` panels): admission, state, output, stop. */
876
+ tasks,
877
+ /** Session goals (`/goal`): state, user actions, the continuation controller. */
878
+ goals,
879
+ /** Python discovery and job folders of `python_run`; `enabled` is false under --read-only. */
880
+ analysis: {
881
+ enabled: analysisEnabled,
882
+ runtime: analysisRuntime,
883
+ jobs: analysisJobs,
884
+ /** Container runtime of the current configuration (status, pull); mode `managed` ignores it. */
885
+ oci,
886
+ rerun: analysisRerun,
887
+ janitor,
888
+ },
889
+ /** Keys a project layer set that only the user layer decides (ignored on load). */
890
+ configDiagnostics: configProvenance.ignored ?? [],
891
+ /** Tabular datasets of every session (ingestion, read-only queries, pages). */
892
+ datasets,
583
893
  registry,
584
894
  context,
585
895
  skills,
@@ -672,12 +982,15 @@ export async function createApplication(options = {}) {
672
982
  mcpStartupFailures() {
673
983
  return [...mcpStartupFailures];
674
984
  },
675
- /** Host-owned interactive consent boundary. Headless callers remain blocked unless flagged. */
985
+ /**
986
+ * Host-owned interactive consent boundary: the TUI and the web UI (`alisio serve`, after an
987
+ * explicit confirmation dialog) may grant. Headless callers remain blocked unless flagged.
988
+ */
676
989
  grantMcpRuntimePermission(request) {
677
990
  if (!request.confirmed)
678
991
  return mcpRuntimePermission;
679
- if (request.source !== "interactive-tui")
680
- throw new Error("MCP runtime permission may only be granted by the interactive TUI");
992
+ if (request.source !== "interactive-tui" && request.source !== "interactive-web")
993
+ throw new Error("MCP runtime permission may only be granted by an interactive UI");
681
994
  if (options.readOnly)
682
995
  throw new Error("MCP is unavailable under --read-only");
683
996
  if (mcpRuntimePermission === "granted")
@@ -688,12 +1001,12 @@ export async function createApplication(options = {}) {
688
1001
  return mcpRuntimePermission;
689
1002
  },
690
1003
  /** Persist global consent atomically, then grant runtime permission for this process. */
691
- async rememberGlobalMcpConsent() {
1004
+ async rememberGlobalMcpConsent(source = "interactive-tui") {
692
1005
  if (options.readOnly)
693
1006
  throw new Error("MCP is unavailable under --read-only");
694
1007
  await setGlobalMcpAllow({ allow: true });
695
1008
  persistedMcpAllow = true;
696
- this.grantMcpRuntimePermission({ source: "interactive-tui", confirmed: true });
1009
+ this.grantMcpRuntimePermission({ source, confirmed: true });
697
1010
  },
698
1011
  /**
699
1012
  * Clear the persisted global consent atomically and drop the runtime permission (disconnecting
@@ -736,6 +1049,8 @@ export async function createApplication(options = {}) {
736
1049
  * (next instructions load), `websearch.provider` mutates the shared config object the search
737
1050
  * chain reads per call, `pluginHooks.timeoutMs` lands in the plugin host (next hook call),
738
1051
  * and `tui.*` is persisted for the host to apply. Nothing in this set requires a restart.
1052
+ * `agents.*` (active agent id and reasoning effort) is persisted for the TUI to consume:
1053
+ * the next `/agents`/`/effort` selection and the next prompt already read the live config.
739
1054
  * `mcp.allow` is deliberately not routed here: it flows through
740
1055
  * `rememberGlobalMcpConsent`/`revokeGlobalMcpConsent` instead.
741
1056
  * Throws under `--read-only`. Returns the written config file.
@@ -767,6 +1082,9 @@ export async function createApplication(options = {}) {
767
1082
  case "limits.maxTurns":
768
1083
  case "limits.maxOutputTokens":
769
1084
  case "limits.maxContextChars":
1085
+ case "limits.firstTokenTimeoutMs":
1086
+ case "limits.firstTokenRetries":
1087
+ case "limits.truncationRecoveries":
770
1088
  case "limits.timeoutMs": {
771
1089
  const patch = limitLeaf(key.slice("limits.".length));
772
1090
  config.limits = { ...config.limits, ...patch };
@@ -780,9 +1098,74 @@ export async function createApplication(options = {}) {
780
1098
  case "tui.paddingX":
781
1099
  config.tui = { ...config.tui, paddingX: Number(value) };
782
1100
  break;
1101
+ case "tui.contentPaddingX":
1102
+ config.tui = { ...config.tui, contentPaddingX: Number(value) };
1103
+ break;
783
1104
  case "tui.skillSlashCommands":
784
1105
  config.tui = { ...config.tui, skillSlashCommands: value === true };
785
1106
  break;
1107
+ case "agents.active":
1108
+ config.agents = { ...config.agents, active: String(value) };
1109
+ break;
1110
+ case "agents.effort":
1111
+ // Clearing (undefined) drops the leaf from the live config and the JSON file.
1112
+ config.agents =
1113
+ value === undefined
1114
+ ? { active: config.agents.active }
1115
+ : { ...config.agents, effort: String(value) };
1116
+ break;
1117
+ case "analysis.enabled":
1118
+ // Tools are registered when the application starts: the change applies to the next
1119
+ // application (a restart; the web recycles the workspace application).
1120
+ config.analysis = { ...config.analysis, enabled: value === true };
1121
+ break;
1122
+ case "analysis.limits.timeoutMs":
1123
+ // Read live by python_run on its next call.
1124
+ config.analysis = {
1125
+ ...config.analysis,
1126
+ limits: { ...config.analysis.limits, timeoutMs: Number(value) },
1127
+ };
1128
+ break;
1129
+ case "analysis.retention.jobsDays":
1130
+ case "analysis.retention.intermediateDays":
1131
+ case "analysis.retention.artifactsDays": {
1132
+ // Read by the next sweep.
1133
+ const leaf = key.slice("analysis.retention.".length);
1134
+ config.analysis = {
1135
+ ...config.analysis,
1136
+ retention: { ...config.analysis.retention, [leaf]: Number(value) },
1137
+ };
1138
+ break;
1139
+ }
1140
+ case "goal.enabled":
1141
+ case "goal.maxTurns":
1142
+ case "goal.maxMinutes":
1143
+ case "goal.repeatedReplyLimit":
1144
+ case "goal.noToolTurnsLimit":
1145
+ case "goal.blockedRepeats": {
1146
+ // Read live: the next continuation (or the next `/goal`) uses the new value. The caps
1147
+ // of a goal already running were snapshotted when it was created.
1148
+ const leaf = key.slice("goal.".length);
1149
+ config.goal = {
1150
+ ...config.goal,
1151
+ [leaf]: leaf === "enabled" ? value === true : Number(value),
1152
+ };
1153
+ break;
1154
+ }
1155
+ case "tasks.enabled":
1156
+ // Tools are registered when the application starts: the change applies to the next
1157
+ // application (a restart; the web recycles the workspace application).
1158
+ config.tasks = { ...config.tasks, enabled: value === true };
1159
+ break;
1160
+ case "tasks.maxPerSession":
1161
+ case "tasks.maxRunMs":
1162
+ case "tasks.maxOutputBytes":
1163
+ case "tasks.retentionDays": {
1164
+ // Read live: the next task (or the next sweep) uses the new value.
1165
+ const leaf = key.slice("tasks.".length);
1166
+ config.tasks = { ...config.tasks, [leaf]: Number(value) };
1167
+ break;
1168
+ }
786
1169
  case "websearch.provider":
787
1170
  // Mutated in place (same object identity) so the tool chain's per-call
788
1171
  // `websearchCtx.config` sees the new provider on the very next search call.
@@ -802,6 +1185,40 @@ export async function createApplication(options = {}) {
802
1185
  return providerInfo;
803
1186
  },
804
1187
  runner,
1188
+ /** `/btw` side questions: tool-less answers about a session outside its conversation. */
1189
+ sideQuestions: new SideQuestions({ runner, state: store }),
1190
+ /**
1191
+ * User agent files (`<workspace>/.agents/agents`, `~/.agents/agents`). Every write asks the
1192
+ * subagents plugin (the runtime agent registry) to rediscover through its `/agents reload`
1193
+ * verb and reports whether the saved file is loaded; without the plugin nothing reloads.
1194
+ */
1195
+ agentDefinitions: new AgentDefinitionService({
1196
+ workspace,
1197
+ home: homedir(),
1198
+ registry: {
1199
+ async reload() {
1200
+ const handler = plugins.commands.get("agents");
1201
+ if (!handler)
1202
+ return false;
1203
+ await handler("reload");
1204
+ return Array.isArray(plugins.pluginState("subagents", "definitions"));
1205
+ },
1206
+ loadedPaths() {
1207
+ let state;
1208
+ try {
1209
+ state = plugins.pluginState("subagents", "definitions");
1210
+ }
1211
+ catch {
1212
+ return undefined;
1213
+ }
1214
+ if (!Array.isArray(state))
1215
+ return undefined;
1216
+ return state
1217
+ .map((entry) => entry?.path)
1218
+ .filter((path) => typeof path === "string");
1219
+ },
1220
+ },
1221
+ }),
805
1222
  herdr,
806
1223
  contextWindow,
807
1224
  /** Effective context budget + compaction point shared by the runner and the TUI bar. */
@@ -900,24 +1317,34 @@ export async function createApplication(options = {}) {
900
1317
  });
901
1318
  },
902
1319
  async close() {
1320
+ // Exit latency is user-facing: tear down herdr + MCP in parallel, then provider +
1321
+ // plugins (store last). Every stage is raced against a cap so a single hanging
1322
+ // component cannot stall /exit; teardown errors are swallowed for callers.
1323
+ const capped = (work) => Promise.race([work.catch(() => { }), delay(TEARDOWN_STAGE_TIMEOUT_MS)]);
903
1324
  try {
904
- await herdr.close();
905
- await mcp.close();
1325
+ janitor.stop();
1326
+ taskJanitor.stop();
1327
+ datasets?.close();
1328
+ // Tasks first in the same stage: they end with Alisio (process groups killed).
1329
+ await capped(Promise.allSettled([herdr.close(), mcp.close(), tasks.close()]));
906
1330
  }
907
1331
  finally {
908
1332
  try {
909
- await provider.dispose();
910
- await Promise.allSettled([
911
- ...new Set([
1333
+ await capped((async () => {
1334
+ const runtimes = new Set([
912
1335
  ...(await Promise.allSettled(targetProviders.values()))
913
1336
  .filter((x) => x.status === "fulfilled")
914
1337
  .map((x) => x.value),
915
1338
  ...retainedProviders,
916
- ]),
917
- ]
918
- .filter((runtime) => runtime !== provider.currentProvider)
919
- .map((runtime) => Promise.resolve(runtime.dispose?.())));
920
- await plugins.close();
1339
+ ]);
1340
+ await Promise.allSettled([
1341
+ provider.dispose(),
1342
+ ...[...runtimes]
1343
+ .filter((runtime) => runtime !== provider.currentProvider)
1344
+ .map((runtime) => Promise.resolve(runtime.dispose?.())),
1345
+ plugins.close(),
1346
+ ]);
1347
+ })());
921
1348
  }
922
1349
  finally {
923
1350
  store.close();
@@ -927,9 +1354,10 @@ export async function createApplication(options = {}) {
927
1354
  };
928
1355
  }
929
1356
  catch (e) {
930
- await herdr.close();
931
- await mcp.close();
932
- await plugins.close();
1357
+ await Promise.race([
1358
+ Promise.allSettled([herdr.close(), mcp.close(), plugins.close()]),
1359
+ delay(TEARDOWN_STAGE_TIMEOUT_MS),
1360
+ ]);
933
1361
  store.close();
934
1362
  throw e;
935
1363
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Artifact type registry: the kind, the served MIME type and whether a rich client may preview a
3
+ * published file. The type comes from the extension AND the leading bytes; when they disagree the
4
+ * most restrictive type wins (`file`, download only). The served `Content-Type` always comes from
5
+ * here, never from the script that wrote the file.
6
+ */
7
+ import type { ArtifactKind } from "@alisio/sdk";
8
+ export interface ArtifactType {
9
+ kind: ArtifactKind;
10
+ mimeType: string;
11
+ }
12
+ export declare const DOWNLOAD_ONLY: ArtifactType;
13
+ /** Lower-case extension without the dot (`""` when none). */
14
+ export declare const extensionOf: (fileName: string) => string;
15
+ /** Text: no NUL byte in the sampled head and the sample decodes as UTF-8. */
16
+ export declare function looksLikeText(head: Uint8Array): boolean;
17
+ /**
18
+ * The type of a file from its name and its first bytes (at least 4 KiB is a good sample). An
19
+ * unknown extension, or bytes that contradict the extension, yield `file` (download only).
20
+ * An empty file keeps the type of its extension.
21
+ */
22
+ export declare function classifyArtifact(fileName: string, head: Uint8Array): ArtifactType;
23
+ /** Whether a rich client may preview a file of this name, type and size. */
24
+ export declare function isPreviewable(fileName: string, type: ArtifactType, bytes: number): boolean;
25
+ /** English labels of each kind (the web localizes its own; text surfaces use these). */
26
+ export declare const KIND_LABELS: Record<ArtifactKind, string>;
27
+ /** The type the sandboxed viewer serves for one file (`application/octet-stream` when unknown). */
28
+ export declare const viewerContentType: (fileName: string) => string;
29
+ /**
30
+ * The registry type of one file inside an artifact (for the authenticated `files/*` route):
31
+ * the extension and the leading bytes decide, exactly like a published file.
32
+ */
33
+ export declare function fileType(fileName: string, head: Uint8Array): ArtifactType & {
34
+ inline: boolean;
35
+ };