@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
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { chmod, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
1
+ import { chmod, mkdir, open, readFile, realpath, rename, unlink } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { z } from "zod";
@@ -75,12 +75,25 @@ const serversSchema = (value) => z.record(z.string(), value).superRefine((server
75
75
  message: "MCP server names may contain only letters, numbers, dot, underscore and dash",
76
76
  });
77
77
  });
78
+ /**
79
+ * A container image reference pinned by digest. Anchored at the start with a letter or digit so a
80
+ * value can never be read as an option by the container CLI.
81
+ */
82
+ export const OCI_IMAGE_PATTERN = /^[a-zA-Z0-9][^\s@]*@sha256:[a-f0-9]{64}$/;
83
+ /** Analysis keys only the user (global) layer decides; a project layer cannot set them. */
84
+ const GLOBAL_ONLY_ANALYSIS = ["runtime", "oci", "retention"];
78
85
  const configObjectSchema = z
79
86
  .object({
80
87
  schemaVersion: z.literal(1).default(1),
81
88
  provider: providerSchema.default(() => providerSchema.parse({})),
82
89
  plugins: z.array(z.string()).default([]),
83
90
  skills: z.array(z.string()).default([]),
91
+ /**
92
+ * Extra directories the mediated path policy may touch outside the workspace. Resolved
93
+ * relative to the defining configuration file and canonicalized on load. Additive across
94
+ * layers like `plugins`/`skills`: a lower layer can only ADD, never clear the global list.
95
+ */
96
+ additionalDirectories: z.array(z.string().trim().min(1)).default([]),
84
97
  /** Project-local enable/disable overrides for effective non-plugin skills. */
85
98
  skillOverrides: z.record(z.string(), z.object({ enabled: z.boolean() }).strict()).default({}),
86
99
  mcp: z
@@ -140,7 +153,15 @@ const configObjectSchema = z
140
153
  websearch: z
141
154
  .object({
142
155
  provider: z
143
- .enum(["searxng", "duckduckgo-instant", "tavily", "brave", "serpapi", "native"])
156
+ .enum([
157
+ "searxng",
158
+ "duckduckgo-instant",
159
+ "duckduckgo-html",
160
+ "tavily",
161
+ "brave",
162
+ "serpapi",
163
+ "native",
164
+ ])
144
165
  .optional(),
145
166
  searxngUrl: z.string().optional(),
146
167
  apiKeyEnv: z.string().optional(),
@@ -151,23 +172,231 @@ const configObjectSchema = z
151
172
  .default(() => ({ nativeToolType: "web_search" })),
152
173
  limits: z
153
174
  .object({
154
- maxTurns: z.number().int().min(1).max(100).default(20),
155
- timeoutMs: z.number().int().min(100).default(300000),
156
- maxContextChars: z.number().int().positive().default(160000),
157
- maxOutputTokens: z.number().int().positive().default(4096),
175
+ maxTurns: z.number().int().min(1).max(100).default(100),
176
+ timeoutMs: z.number().int().min(100).default(600000),
177
+ /**
178
+ * Stop a model request that stays completely silent (no text, reasoning or tool-call
179
+ * delta) this long, so a queued or stuck provider is cut after 90 s instead of the
180
+ * whole-run `timeoutMs`. 0 disables it (models that do not stream their reasoning can stay
181
+ * silent for minutes: raise or disable it for those).
182
+ */
183
+ firstTokenTimeoutMs: z.number().int().min(0).default(90000),
184
+ /**
185
+ * How many times the SAME request is sent again after it stayed silent for
186
+ * `firstTokenTimeoutMs` (a stalled connection usually answers on the next try). 0
187
+ * disables the retry. Never counts as a turn.
188
+ */
189
+ firstTokenRetries: z.number().int().min(0).max(3).default(1),
190
+ /**
191
+ * How many times a turn is requested again after the model's response was cut off by
192
+ * `maxOutputTokens` before it was usable (no text and no tool call, or a tool call with
193
+ * incomplete arguments). The truncated calls are never executed. 0 disables the
194
+ * recovery. Never counts as a turn.
195
+ */
196
+ truncationRecoveries: z.number().int().min(0).max(5).default(2),
197
+ maxContextChars: z.number().int().positive().default(800000),
198
+ /**
199
+ * Per-call output token budget for one agent turn. Deliberately WITHOUT a schema default:
200
+ * when unset, the budget of each request is the model catalog's declared maximum (capped
201
+ * at 65536) or 16384 (see `core/output-limit.ts`), so "set by the user" stays
202
+ * distinguishable from "defaulted". A value set here always wins. 4096 starves
203
+ * reasoning-heavy models, which can spend the whole budget on `reasoning_content`
204
+ * before any usable text arrives.
205
+ */
206
+ maxOutputTokens: z.number().int().positive().optional(),
158
207
  /** Cumulative tokens per run; default is proportional to the context window. */
159
208
  maxTokens: z.number().int().positive().optional(),
160
209
  })
161
210
  .default(() => ({
162
- maxTurns: 20,
163
- timeoutMs: 300000,
164
- maxContextChars: 160000,
165
- maxOutputTokens: 4096,
211
+ maxTurns: 100,
212
+ timeoutMs: 600000,
213
+ firstTokenTimeoutMs: 90000,
214
+ firstTokenRetries: 1,
215
+ truncationRecoveries: 2,
216
+ maxContextChars: 800000,
217
+ })),
218
+ /**
219
+ * Python analysis and downloadable artifacts. `enabled: false` registers none of the
220
+ * analysis tools. The interpreter is discovered automatically (or pinned with `--python`).
221
+ */
222
+ analysis: z
223
+ .object({
224
+ enabled: z.boolean().default(true),
225
+ /**
226
+ * Where `python_run` executes. `managed` is the discovered Python (not a sandbox); `oci`
227
+ * is a Docker or Podman container started from a pinned image. GLOBAL ONLY: a project
228
+ * layer cannot choose the runtime or the image (it is ignored with a diagnostic).
229
+ */
230
+ runtime: z.enum(["managed", "oci"]).default("managed"),
231
+ /** Container runtime settings (global only). Used when `runtime` is `oci`. */
232
+ oci: z
233
+ .object({
234
+ engine: z.enum(["docker", "podman"]).default("docker"),
235
+ /** `name@sha256:<digest>`: an image without a digest is rejected at load time. */
236
+ image: z
237
+ .string()
238
+ .regex(OCI_IMAGE_PATTERN, "analysis.oci.image must be pinned by digest: <name>@sha256:<64 hex characters>")
239
+ .optional(),
240
+ memoryMb: z.number().int().min(256).max(65_536).default(2048),
241
+ cpus: z.number().min(0.5).max(64).default(2),
242
+ })
243
+ .strict()
244
+ .default(() => ({ engine: "docker", memoryMb: 2048, cpus: 2 })),
245
+ limits: z
246
+ .object({
247
+ /** Default and maximum run time of one python_run call. */
248
+ timeoutMs: z.number().int().min(1000).max(900_000).default(120_000),
249
+ /** Files published by one execution. */
250
+ maxFiles: z.number().int().min(1).max(1000).default(200),
251
+ maxFileBytes: z
252
+ .number()
253
+ .int()
254
+ .positive()
255
+ .default(100 * 1024 * 1024),
256
+ /** Total bytes published by one execution. */
257
+ maxOutputBytes: z
258
+ .number()
259
+ .int()
260
+ .positive()
261
+ .default(500 * 1024 * 1024),
262
+ /** Size cap of each stdout/stderr log of a job. */
263
+ maxLogBytes: z
264
+ .number()
265
+ .int()
266
+ .positive()
267
+ .default(10 * 1024 * 1024),
268
+ })
269
+ .strict()
270
+ .default(() => ({
271
+ timeoutMs: 120_000,
272
+ maxFiles: 200,
273
+ maxFileBytes: 100 * 1024 * 1024,
274
+ maxOutputBytes: 500 * 1024 * 1024,
275
+ maxLogBytes: 10 * 1024 * 1024,
276
+ })),
277
+ /** Tabular data (CSV, TSV, JSON, JSONL, XLSX) ingested into one SQLite file per dataset. */
278
+ data: z
279
+ .object({
280
+ /** Largest file ingested (an upload or a workspace file). */
281
+ maxUploadBytes: z
282
+ .number()
283
+ .int()
284
+ .positive()
285
+ .default(200 * 1024 * 1024),
286
+ /** Rows per sheet; a larger file stops the ingestion and leaves no dataset. */
287
+ maxRows: z.number().int().positive().default(5_000_000),
288
+ /** Time limit of one `data_query` statement; the engine process is killed past it. */
289
+ queryTimeoutMs: z.number().int().min(100).max(60_000).default(5000),
290
+ /** Sorting and filtering are disabled above this many rows (the file has no indexes). */
291
+ maxInteractiveRows: z.number().int().positive().default(1_000_000),
292
+ })
293
+ .strict()
294
+ .default(() => ({
295
+ maxUploadBytes: 200 * 1024 * 1024,
296
+ maxRows: 5_000_000,
297
+ queryTimeoutMs: 5000,
298
+ maxInteractiveRows: 1_000_000,
299
+ })),
300
+ /**
301
+ * Retention of analysis data (`0` disables the automatic deletion of that class). Global
302
+ * only: one sweep covers every workspace, so a project layer cannot shorten it.
303
+ */
304
+ retention: z
305
+ .object({
306
+ /** Logs and staging of a job; its script and inputs once no artifact is `ready`. */
307
+ jobsDays: z.number().int().min(0).max(3650).default(30),
308
+ /** The scratch folder (`work/`) of a job. */
309
+ intermediateDays: z.number().int().min(0).max(3650).default(7),
310
+ /** Artifacts older than this become `expired` (folder deleted, row kept). 0 = keep. */
311
+ artifactsDays: z.number().int().min(0).max(3650).default(0),
312
+ })
313
+ .strict()
314
+ .default(() => ({ jobsDays: 30, intermediateDays: 7, artifactsDays: 0 })),
315
+ })
316
+ .strict()
317
+ .default(() => ({
318
+ enabled: true,
319
+ runtime: "managed",
320
+ oci: { engine: "docker", memoryMb: 2048, cpus: 2 },
321
+ retention: { jobsDays: 30, intermediateDays: 7, artifactsDays: 0 },
322
+ limits: {
323
+ timeoutMs: 120_000,
324
+ maxFiles: 200,
325
+ maxFileBytes: 100 * 1024 * 1024,
326
+ maxOutputBytes: 500 * 1024 * 1024,
327
+ maxLogBytes: 10 * 1024 * 1024,
328
+ },
329
+ data: {
330
+ maxUploadBytes: 200 * 1024 * 1024,
331
+ maxRows: 5_000_000,
332
+ queryTimeoutMs: 5000,
333
+ maxInteractiveRows: 1_000_000,
334
+ },
335
+ })),
336
+ /**
337
+ * Background tasks (`bg_run`, `bg_list`, `bg_output`, `bg_stop`). They are ordinary child
338
+ * processes of Alisio (not detached, not sandboxed): they end when Alisio ends. `enabled:
339
+ * false` registers none of the tools (applies from the next start). `retentionDays` is
340
+ * global-only: one sweep covers every workspace, so a project layer cannot shorten it.
341
+ */
342
+ tasks: z
343
+ .object({
344
+ enabled: z.boolean().default(true),
345
+ /** Live (queued, running or stopping) tasks per root session. */
346
+ maxPerSession: z.number().int().min(1).max(32).default(4),
347
+ /** Watchdog: a task still running after this long is stopped and fails with `timeout`. */
348
+ maxRunMs: z.number().int().min(1000).max(86_400_000).default(3_600_000),
349
+ /** Size limit of one task log (the head is kept, a short tail is added when it ends). */
350
+ maxOutputBytes: z.number().int().min(65_536).max(104_857_600).default(2_097_152),
351
+ /** Finished task rows and their logs are deleted after this many days (0 keeps them). */
352
+ retentionDays: z.number().int().min(0).max(3650).default(7),
353
+ })
354
+ .strict()
355
+ .default(() => ({
356
+ enabled: true,
357
+ maxPerSession: 4,
358
+ maxRunMs: 3_600_000,
359
+ maxOutputBytes: 2_097_152,
360
+ retentionDays: 7,
361
+ })),
362
+ /**
363
+ * `/goal`: a session objective the runtime keeps working on, with hard caps. The token budget
364
+ * is NOT a setting (none unless the user gives one with `budget=`); without it only the turn
365
+ * and time caps stop a goal. `maxTurns` counts runs (a goal turn is one run); `maxMinutes` is
366
+ * the time spent inside runs. The breakers pause a goal that makes no progress.
367
+ */
368
+ goal: z
369
+ .object({
370
+ enabled: z.boolean().default(true),
371
+ maxTurns: z.number().int().min(1).max(1000).default(50),
372
+ maxMinutes: z.number().int().min(1).max(1440).default(120),
373
+ /** Pause after this many consecutive identical replies (observed, nudged, then paused). */
374
+ repeatedReplyLimit: z.number().int().min(2).max(20).default(3),
375
+ /** Pause after this many consecutive turns without a tool call. */
376
+ noToolTurnsLimit: z.number().int().min(2).max(20).default(3),
377
+ /** Consecutive turns in which the model must report the same blocker before it stops. */
378
+ blockedRepeats: z.number().int().min(1).max(10).default(2),
379
+ })
380
+ .strict()
381
+ .default(() => ({
382
+ enabled: true,
383
+ maxTurns: 50,
384
+ maxMinutes: 120,
385
+ repeatedReplyLimit: 3,
386
+ noToolTurnsLimit: 3,
387
+ blockedRepeats: 2,
166
388
  })),
167
389
  tui: z
168
390
  .object({
169
- /** Horizontal padding (columns) around the editor input box. */
391
+ /** Horizontal padding (columns) around the editor input box. Editor-only. */
170
392
  paddingX: z.number().int().min(0).max(4).default(1),
393
+ /**
394
+ * Horizontal inset (columns) applied to each side of the transcript content, so
395
+ * agent-written rows keep breathing room from the window borders. Independent of
396
+ * `paddingX` (which styles only the editor input box). Clamped on narrow terminals so the
397
+ * content column never collapses below a usable width.
398
+ */
399
+ contentPaddingX: z.number().int().min(0).max(12).default(2),
171
400
  /**
172
401
  * Offer effective skills as first-class `skill:<id>` editor slash-autocomplete entries.
173
402
  * Off hides those entries; the `/skills` manager and its argument completion stay available.
@@ -175,7 +404,28 @@ const configObjectSchema = z
175
404
  skillSlashCommands: z.boolean().default(true),
176
405
  })
177
406
  .strict()
178
- .default(() => ({ paddingX: 1, skillSlashCommands: true })),
407
+ .default(() => ({ paddingX: 1, contentPaddingX: 2, skillSlashCommands: true })),
408
+ /**
409
+ * The ACTIVE agent drives the main session: its system prompt is appended to every prompt and
410
+ * its optional model selector is offered when the agent is chosen (`/agents`). `build` is the
411
+ * built-in default (full-power, no added persona); `plan` is the built-in read-only planner.
412
+ * `effort` holds a reasoning effort level chosen with `/effort` for a model that advertises
413
+ * `effort.supportedLevels`; it is validated against the ACTIVE model on use, falling back to
414
+ * the model's default level when unsupported.
415
+ */
416
+ agents: z
417
+ .object({
418
+ active: z
419
+ .string()
420
+ .trim()
421
+ .min(1)
422
+ .max(100)
423
+ .regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/, "invalid agent name")
424
+ .default("build"),
425
+ effort: z.string().trim().min(1).max(50).optional(),
426
+ })
427
+ .strict()
428
+ .default(() => ({ active: "build" })),
179
429
  })
180
430
  .strict()
181
431
  .superRefine((config, context) => {
@@ -204,6 +454,26 @@ function isUsableLegacyModel(model) {
204
454
  const normalized = model.trim();
205
455
  return normalized !== "" && normalized !== LEGACY_PROVIDER_MODEL_PLACEHOLDER;
206
456
  }
457
+ /**
458
+ * Appends the entries of `selected` after the `global` list, dropping exact duplicates so the
459
+ * global entries keep their order and the selected layer only ever ADDS to the collection. An
460
+ * empty `selected` list therefore leaves the global list intact. Entries were already resolved
461
+ * per layer by `parseLayer`, so deduplication happens on the resolved strings.
462
+ */
463
+ function mergeUnique(global, selected) {
464
+ const seen = new Set(global);
465
+ const merged = [...global];
466
+ for (const entry of selected)
467
+ if (!seen.has(entry)) {
468
+ seen.add(entry);
469
+ merged.push(entry);
470
+ }
471
+ return merged;
472
+ }
473
+ /** Absolute, canonical directory when it exists; a missing path keeps its resolved absolute form. */
474
+ async function canonicalDirectory(path) {
475
+ return realpath(path).catch(() => resolve(path));
476
+ }
207
477
  /**
208
478
  * Whether legacy provider selection should take priority over a saved plugin profile for this
209
479
  * run: an explicit endpoint override, or a trusted project/explicit layer whose parsed root
@@ -236,6 +506,9 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
236
506
  const raw = (await readJson(file)) ?? {};
237
507
  const config = configSchema.parse(raw);
238
508
  config.skills = config.skills.map((p) => resolve(file, "..", p));
509
+ // Extra tool roots are paths relative to the config file; canonicalize so containment checks
510
+ // compare real directories (a symlinked root never escapes its declared location).
511
+ config.additionalDirectories = await Promise.all(config.additionalDirectories.map((p) => canonicalDirectory(resolve(file, "..", p))));
239
512
  // Plugin entries are paths (relative to the config file) or npm package names.
240
513
  config.plugins = config.plugins.map((p) => (isPathSpec(p) ? resolve(file, "..", p) : p));
241
514
  for (const server of Object.values(config.mcp.servers))
@@ -259,7 +532,17 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
259
532
  form: canonical && name in canonical ? "canonical" : "alias",
260
533
  },
261
534
  ]));
262
- return { config, keys: new Set(Object.keys(object)), sources };
535
+ const analysisRaw = object.analysis && typeof object.analysis === "object" && !Array.isArray(object.analysis)
536
+ ? object.analysis
537
+ : {};
538
+ const tasksRaw = object.tasks && typeof object.tasks === "object" && !Array.isArray(object.tasks)
539
+ ? object.tasks
540
+ : {};
541
+ const ignored = [
542
+ ...GLOBAL_ONLY_ANALYSIS.filter((key) => key in analysisRaw).map((key) => `analysis.${key}`),
543
+ ...("retentionDays" in tasksRaw ? ["tasks.retentionDays"] : []),
544
+ ];
545
+ return { config, keys: new Set(Object.keys(object)), sources, ignored, file };
263
546
  };
264
547
  const global = await parseLayer(globalFile, "global");
265
548
  // Skill activation is deliberately project-local; never carry a similarly named global field
@@ -270,15 +553,72 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
270
553
  // Parsed `provider.model` of the selected layer itself (before env/CLI overrides), used to
271
554
  // decide whether its legacy root `provider` is usable against a saved `/connect` profile.
272
555
  let selectedLegacyModel = "";
556
+ let ignoredKeys = [];
273
557
  if (selectedFile && selectedFile !== globalFile) {
274
558
  const selected = await parseLayer(selectedFile, options.file ? "explicit" : "project");
559
+ ignoredKeys = selected.ignored;
275
560
  selectedLegacyModel = selected.config.provider.model;
276
561
  const overlaid = { ...config };
277
562
  for (const key of selected.keys) {
278
563
  if (key === "mcp" || key === "mcpServers")
279
564
  continue;
280
- if (key in selected.config)
281
- overlaid[key] = selected.config[key];
565
+ if (!(key in selected.config))
566
+ continue;
567
+ // Additive collections: the selected layer ADDS to the global list (global entries
568
+ // first, exact duplicates dropped); an empty selected list never clears the global one.
569
+ if (key === "plugins") {
570
+ overlaid.plugins = mergeUnique(overlaid.plugins, selected.config.plugins);
571
+ continue;
572
+ }
573
+ if (key === "skills") {
574
+ overlaid.skills = mergeUnique(overlaid.skills, selected.config.skills);
575
+ continue;
576
+ }
577
+ if (key === "additionalDirectories") {
578
+ overlaid.additionalDirectories = mergeUnique(overlaid.additionalDirectories, selected.config.additionalDirectories);
579
+ continue;
580
+ }
581
+ // Additive records: merge by key, the selected layer wins per key.
582
+ if (key === "pluginOverrides") {
583
+ overlaid.pluginOverrides = {
584
+ ...overlaid.pluginOverrides,
585
+ ...selected.config.pluginOverrides,
586
+ };
587
+ continue;
588
+ }
589
+ if (key === "skillOverrides") {
590
+ // Global skill overrides were reset above (skill activation is project-local); the
591
+ // selected overrides still merge into that empty record and win per key.
592
+ overlaid.skillOverrides = { ...overlaid.skillOverrides, ...selected.config.skillOverrides };
593
+ continue;
594
+ }
595
+ if (key === "builtinPlugins") {
596
+ // Per-plugin options merge by plugin id: the selected entry wins per id, so
597
+ // `{id: {enabled: false}}` in the selected layer still disables that built-in while
598
+ // every other global entry stays.
599
+ overlaid.builtinPlugins = { ...overlaid.builtinPlugins, ...selected.config.builtinPlugins };
600
+ continue;
601
+ }
602
+ if (key === "analysis") {
603
+ // `runtime`, `oci` and `retention` are global-only (a repository must not pick the binary
604
+ // that runs scripts, nor shorten the retention of every workspace's data).
605
+ overlaid.analysis = {
606
+ ...selected.config.analysis,
607
+ runtime: global.config.analysis.runtime,
608
+ oci: global.config.analysis.oci,
609
+ retention: global.config.analysis.retention,
610
+ };
611
+ continue;
612
+ }
613
+ if (key === "tasks") {
614
+ // `retentionDays` is global-only (one sweep deletes the data of every workspace).
615
+ overlaid.tasks = {
616
+ ...selected.config.tasks,
617
+ retentionDays: global.config.tasks.retentionDays,
618
+ };
619
+ continue;
620
+ }
621
+ overlaid[key] = selected.config[key];
282
622
  }
283
623
  if (selected.keys.has("mcp") || selected.keys.has("mcpServers"))
284
624
  // Only servers merge upward; `mcp.allow` is a global/user preference and is deliberately
@@ -316,6 +656,7 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
316
656
  },
317
657
  }
318
658
  : {}),
659
+ ...(ignoredKeys.length ? { ignored: ignoredKeys } : {}),
319
660
  },
320
661
  };
321
662
  }
@@ -422,7 +763,13 @@ const SETTABLE_SECTIONS = {
422
763
  pluginHooks: configObjectSchema.shape.pluginHooks.removeDefault(),
423
764
  tui: configObjectSchema.shape.tui.removeDefault(),
424
765
  websearch: configObjectSchema.shape.websearch.removeDefault(),
766
+ agents: configObjectSchema.shape.agents.removeDefault(),
767
+ analysis: configObjectSchema.shape.analysis.removeDefault(),
768
+ tasks: configObjectSchema.shape.tasks.removeDefault(),
769
+ goal: configObjectSchema.shape.goal.removeDefault(),
425
770
  };
771
+ const ANALYSIS_LIMITS = SETTABLE_SECTIONS.analysis.shape.limits.removeDefault();
772
+ const ANALYSIS_RETENTION = SETTABLE_SECTIONS.analysis.shape.retention.removeDefault();
426
773
  const SETTABLE_KEYS = {
427
774
  "compaction.auto": SETTABLE_SECTIONS.compaction.shape.auto,
428
775
  "compaction.threshold": SETTABLE_SECTIONS.compaction.shape.threshold,
@@ -434,14 +781,63 @@ const SETTABLE_KEYS = {
434
781
  "limits.maxOutputTokens": SETTABLE_SECTIONS.limits.shape.maxOutputTokens,
435
782
  "limits.maxContextChars": SETTABLE_SECTIONS.limits.shape.maxContextChars,
436
783
  "limits.timeoutMs": SETTABLE_SECTIONS.limits.shape.timeoutMs,
784
+ "limits.firstTokenTimeoutMs": SETTABLE_SECTIONS.limits.shape.firstTokenTimeoutMs,
785
+ "limits.firstTokenRetries": SETTABLE_SECTIONS.limits.shape.firstTokenRetries,
786
+ "limits.truncationRecoveries": SETTABLE_SECTIONS.limits.shape.truncationRecoveries,
437
787
  "pluginHooks.timeoutMs": SETTABLE_SECTIONS.pluginHooks.shape.timeoutMs,
438
788
  "tui.paddingX": SETTABLE_SECTIONS.tui.shape.paddingX,
789
+ "tui.contentPaddingX": SETTABLE_SECTIONS.tui.shape.contentPaddingX,
439
790
  "tui.skillSlashCommands": SETTABLE_SECTIONS.tui.shape.skillSlashCommands,
440
791
  "websearch.provider": SETTABLE_SECTIONS.websearch.shape.provider,
792
+ "agents.active": SETTABLE_SECTIONS.agents.shape.active,
793
+ "agents.effort": SETTABLE_SECTIONS.agents.shape.effort,
794
+ "analysis.enabled": SETTABLE_SECTIONS.analysis.shape.enabled,
795
+ "analysis.limits.timeoutMs": ANALYSIS_LIMITS.shape.timeoutMs,
796
+ "analysis.retention.jobsDays": ANALYSIS_RETENTION.shape.jobsDays,
797
+ "analysis.retention.intermediateDays": ANALYSIS_RETENTION.shape.intermediateDays,
798
+ "analysis.retention.artifactsDays": ANALYSIS_RETENTION.shape.artifactsDays,
799
+ "tasks.enabled": SETTABLE_SECTIONS.tasks.shape.enabled,
800
+ "tasks.maxPerSession": SETTABLE_SECTIONS.tasks.shape.maxPerSession,
801
+ "tasks.maxRunMs": SETTABLE_SECTIONS.tasks.shape.maxRunMs,
802
+ "tasks.maxOutputBytes": SETTABLE_SECTIONS.tasks.shape.maxOutputBytes,
803
+ "tasks.retentionDays": SETTABLE_SECTIONS.tasks.shape.retentionDays,
804
+ "goal.enabled": SETTABLE_SECTIONS.goal.shape.enabled,
805
+ "goal.maxTurns": SETTABLE_SECTIONS.goal.shape.maxTurns,
806
+ "goal.maxMinutes": SETTABLE_SECTIONS.goal.shape.maxMinutes,
807
+ "goal.repeatedReplyLimit": SETTABLE_SECTIONS.goal.shape.repeatedReplyLimit,
808
+ "goal.noToolTurnsLimit": SETTABLE_SECTIONS.goal.shape.noToolTurnsLimit,
809
+ "goal.blockedRepeats": SETTABLE_SECTIONS.goal.shape.blockedRepeats,
441
810
  };
442
811
  export function isSettableSettingKey(key) {
443
812
  return Object.prototype.hasOwnProperty.call(SETTABLE_KEYS, key);
444
813
  }
814
+ /** Unwraps optional/default/nullable wrappers down to the value schema. */
815
+ function settingLeaf(schema) {
816
+ let current = schema;
817
+ for (let depth = 0; depth < 8; depth++) {
818
+ const type = current.def?.type;
819
+ if ((type === "optional" || type === "default" || type === "nullable") &&
820
+ current.def?.innerType)
821
+ current = current.def.innerType;
822
+ else
823
+ break;
824
+ }
825
+ return current;
826
+ }
827
+ /** Every settable key with its value kind (and enum options), for settings editors. */
828
+ export function settableSettings() {
829
+ return Object.keys(SETTABLE_KEYS).map((key) => {
830
+ const leaf = settingLeaf(SETTABLE_KEYS[key]);
831
+ const type = leaf.def?.type;
832
+ if (type === "enum")
833
+ return { key, kind: "enum", options: (leaf.options ?? []).map((o) => String(o)) };
834
+ if (type === "boolean")
835
+ return { key, kind: "boolean" };
836
+ if (type === "number" || type === "int")
837
+ return { key, kind: "number" };
838
+ return { key, kind: "string" };
839
+ });
840
+ }
445
841
  /**
446
842
  * Atomically sets one user-facing setting in the global config file (`<config home>/config.json`)
447
843
  * while preserving every unrelated JSON field. The value is validated against the same zod leaf
@@ -467,11 +863,21 @@ export async function setConfigValue(input) {
467
863
  throw new Error("Global Alisio configuration must be a JSON object");
468
864
  raw = existing;
469
865
  }
470
- const [section, leaf] = input.key.split(".");
471
- const container = raw[section] && typeof raw[section] === "object" && !Array.isArray(raw[section])
472
- ? raw[section]
473
- : {};
474
- raw[section] = { ...container, [leaf]: parsed.data };
866
+ // Keys are `section.leaf` or `section.group.leaf`: every object on the path keeps its other fields.
867
+ const path = input.key.split(".");
868
+ const setPath = (target, depth) => {
869
+ const name = path[depth];
870
+ if (depth === path.length - 1)
871
+ return { ...target, [name]: parsed.data };
872
+ const child = target[name];
873
+ return {
874
+ ...target,
875
+ [name]: setPath(child && typeof child === "object" && !Array.isArray(child)
876
+ ? child
877
+ : {}, depth + 1),
878
+ };
879
+ };
880
+ raw = setPath(raw, 0);
475
881
  raw.schemaVersion ??= 1;
476
882
  await mkdir(dirname(file), { recursive: true, mode: 0o700 });
477
883
  const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
@@ -116,6 +116,10 @@ function clampMessage(message, maxToolResultChars, maxTextChars) {
116
116
  let used = 0;
117
117
  let changed = false;
118
118
  const content = message.result.content.map((part) => {
119
+ // Only the text projection is clipped: ui/image parts are display-only transcript data and
120
+ // are never truncated (raw bytes never reach the model, which only sees the projection).
121
+ if (part.type !== "text")
122
+ return part;
119
123
  const room = maxToolResultChars - used;
120
124
  const text = room <= 0
121
125
  ? TRUNCATION_MARKER
@@ -213,7 +217,8 @@ export function reduceMessagesToBudget(messages, targetChars, options = {}) {
213
217
  : m.role === "assistant"
214
218
  ? m.text.length > textCap
215
219
  : m.role === "tool" &&
216
- m.result.content.reduce((a, p) => a + p.text.length, 0) > toolCap;
220
+ m.result.content.reduce((a, p) => a + (p.type === "text" ? p.text.length : 0), 0) >
221
+ toolCap;
217
222
  if (over) {
218
223
  best = i;
219
224
  bestSize = current;
@@ -260,7 +265,12 @@ export function serializeForSummary(messages, maxItemChars = 4_000) {
260
265
  lines.push(`TOOL CALL ${c.id} ${c.name}: ${clip(c.arguments, maxItemChars)}`);
261
266
  }
262
267
  else {
263
- const text = m.result.content.map((c) => c.text).join("\n");
268
+ // Only the text projection is summarized: ui/image parts are display data and never
269
+ // reach the summarizer (raw image bytes in particular stay out of the checkpoint).
270
+ const text = m.result.content
271
+ .filter((p) => p.type === "text")
272
+ .map((c) => c.text)
273
+ .join("\n");
264
274
  lines.push(`TOOL RESULT ${m.callId}${m.result.isError ? " (error)" : ""}: ${clip(text, maxItemChars)}`);
265
275
  }
266
276
  }
@@ -381,6 +391,7 @@ async function completeTextWithMeta(provider, request) {
381
391
  signal: request.signal,
382
392
  ...(request.model ? { model: request.model } : {}),
383
393
  ...(request.sessionId ? { sessionId: request.sessionId } : {}),
394
+ ...(request.reasoningEffort ? { reasoningEffort: request.reasoningEffort } : {}),
384
395
  })) {
385
396
  request.signal.throwIfAborted();
386
397
  if (event.type === "completed") {