@alisio/core 0.1.0-alpha.16 → 0.1.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/active.d.ts +66 -0
- package/dist/agents/active.js +113 -0
- package/dist/application.d.ts +18 -3
- package/dist/application.js +44 -6
- package/dist/commands/builtins.d.ts +10 -0
- package/dist/commands/builtins.js +115 -0
- package/dist/commands/catalog.d.ts +83 -0
- package/dist/commands/catalog.js +268 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +105 -5
- package/dist/core/contracts.d.ts +109 -2
- package/dist/core/runner.d.ts +10 -0
- package/dist/core/runner.js +86 -12
- package/dist/index.d.ts +9 -4
- package/dist/index.js +8 -3
- package/dist/mcp/rich.js +68 -0
- package/dist/plugins/host.d.ts +4 -0
- package/dist/plugins/host.js +22 -8
- package/dist/providers/settings.d.ts +15 -0
- package/dist/providers/settings.js +38 -0
- package/dist/resources/context.js +6 -1
- package/dist/runtime/access.d.ts +62 -0
- package/dist/runtime/access.js +115 -0
- package/dist/runtime/blobs.d.ts +29 -0
- package/dist/runtime/blobs.js +94 -0
- package/dist/runtime/diff.d.ts +23 -0
- package/dist/runtime/diff.js +154 -0
- package/dist/runtime/paths.d.ts +6 -0
- package/dist/runtime/paths.js +11 -1
- package/dist/runtime/store.d.ts +69 -3
- package/dist/runtime/store.js +292 -11
- package/dist/tools/execute.d.ts +5 -0
- package/dist/tools/execute.js +1 -0
- package/dist/tools/standard.d.ts +4 -1
- package/dist/tools/standard.js +99 -18
- package/package.json +2 -2
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { agentCatalogFromState, resolveActiveAgent } from "../agents/active.js";
|
|
2
|
+
import { BUILTIN_COMMANDS } from "./builtins.js";
|
|
3
|
+
export { BUILTIN_COMMANDS };
|
|
4
|
+
const builtinDescriptor = (c) => ({
|
|
5
|
+
name: c.name,
|
|
6
|
+
description: c.description,
|
|
7
|
+
...(c.aliases ? { aliases: [...c.aliases] } : {}),
|
|
8
|
+
...(c.argumentHint ? { argumentHint: c.argumentHint } : {}),
|
|
9
|
+
source: "builtin",
|
|
10
|
+
surfaces: [...c.surfaces],
|
|
11
|
+
execution: c.execution,
|
|
12
|
+
});
|
|
13
|
+
const ALL = ["tui", "web", "api"];
|
|
14
|
+
/** Root sessions of this workspace bound to the current provider, newest first (as the TUI). */
|
|
15
|
+
const workspaceSessions = (host) => host.store
|
|
16
|
+
.list()
|
|
17
|
+
.filter((s) => s.workspace === host.workspace && s.provider === host.provider.id);
|
|
18
|
+
const firstPrompt = (host, id) => {
|
|
19
|
+
const first = host.store.messages(id).find((m) => m.role === "user" && !m.summary);
|
|
20
|
+
return first?.role === "user" ? first.text.replace(/\s+/g, " ").slice(0, 60) : "";
|
|
21
|
+
};
|
|
22
|
+
const HANDLERS = {
|
|
23
|
+
async tools(_args, _ctx, host) {
|
|
24
|
+
const policy = host.runner.policy;
|
|
25
|
+
const rows = host.registry
|
|
26
|
+
.list()
|
|
27
|
+
.map((t) => {
|
|
28
|
+
const effect = t.effect ?? "external";
|
|
29
|
+
const state = effect === "read" || effect === "internal" || policy[effect]
|
|
30
|
+
? "enabled"
|
|
31
|
+
: host.runner.approvals && (effect === "write" || effect === "process")
|
|
32
|
+
? "ask"
|
|
33
|
+
: "disabled";
|
|
34
|
+
return `| \`${t.name}\` | ${effect} | ${state} |`;
|
|
35
|
+
})
|
|
36
|
+
.join("\n");
|
|
37
|
+
return {
|
|
38
|
+
text: `**Tools**\n\n| Tool | Effect | State |\n| --- | --- | --- |\n${rows}\n\n\`ask\` prompts before running (allow once / session / deny). Use --allow-write / --allow-process to pre-allow; --read-only disables them. Paths outside the workspace ask per directory; pre-allow with --add-dir or the \`additionalDirectories\` config key. \`internal\` tools (built-in plugins) only write Alisio's own state.`,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
async sessions(_args, ctx, host) {
|
|
42
|
+
const list = workspaceSessions(host).slice(0, 20);
|
|
43
|
+
if (!list.length)
|
|
44
|
+
return { text: "No sessions in this workspace", tone: "notice" };
|
|
45
|
+
return {
|
|
46
|
+
text: [
|
|
47
|
+
"**Recent sessions** (use `/resume <id-prefix>`)",
|
|
48
|
+
"",
|
|
49
|
+
...list.map((s) => `- \`${s.id}\` ${s.id === ctx.sessionId ? "**(current)** " : ""}${s.model} · ${host.store.messages(s.id).length} msgs · ${firstPrompt(host, s.id) || "_empty_"}`),
|
|
50
|
+
].join("\n"),
|
|
51
|
+
data: list,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
async resume(args, _ctx, host) {
|
|
55
|
+
const target = args.trim();
|
|
56
|
+
if (!target)
|
|
57
|
+
throw new Error("Usage: /resume <id>");
|
|
58
|
+
const matches = workspaceSessions(host).filter((s) => s.id.startsWith(target));
|
|
59
|
+
if (matches.length !== 1)
|
|
60
|
+
throw new Error(matches.length
|
|
61
|
+
? `Ambiguous session prefix: ${target}`
|
|
62
|
+
: `No session in this workspace matches ${target}`);
|
|
63
|
+
const found = matches[0];
|
|
64
|
+
return { text: `Resumed session ${found.id}`, tone: "notice", sessionId: found.id };
|
|
65
|
+
},
|
|
66
|
+
async clear(_args, ctx, host) {
|
|
67
|
+
const model = host.store.get(ctx.sessionId).model;
|
|
68
|
+
const created = host.store.create(host.workspace, host.provider.id, model);
|
|
69
|
+
return { text: `New session ${created.id}`, tone: "notice", sessionId: created.id };
|
|
70
|
+
},
|
|
71
|
+
async compact(args, ctx, host) {
|
|
72
|
+
const result = await host.runner.compact(ctx.sessionId, {
|
|
73
|
+
...(args.trim() ? { focus: args.trim() } : {}),
|
|
74
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
75
|
+
});
|
|
76
|
+
return result
|
|
77
|
+
? {
|
|
78
|
+
text: `Compacted ${result.replaced} messages: ~${result.before} → ~${result.after} tokens`,
|
|
79
|
+
tone: "notice",
|
|
80
|
+
data: result,
|
|
81
|
+
}
|
|
82
|
+
: { text: "Not enough history to compact", tone: "notice" };
|
|
83
|
+
},
|
|
84
|
+
async model(args, ctx, host) {
|
|
85
|
+
const model = args.trim();
|
|
86
|
+
if (!model)
|
|
87
|
+
throw new Error("Usage: /model <model id>");
|
|
88
|
+
host.runner.setModel(ctx.sessionId, model);
|
|
89
|
+
return { text: `Model set to \`${model}\``, tone: "notice", data: { model } };
|
|
90
|
+
},
|
|
91
|
+
async effort(args, _ctx, host) {
|
|
92
|
+
const level = args.trim();
|
|
93
|
+
if (!level)
|
|
94
|
+
return {
|
|
95
|
+
text: `Reasoning effort: ${host.config?.agents.effort ?? "model default"}`,
|
|
96
|
+
tone: "notice",
|
|
97
|
+
};
|
|
98
|
+
if (!host.updateSetting)
|
|
99
|
+
throw new Error("Effort changes are unavailable here");
|
|
100
|
+
const value = level === "!clear" ? undefined : level;
|
|
101
|
+
await host.updateSetting("agents.effort", value);
|
|
102
|
+
return {
|
|
103
|
+
text: value ? `Reasoning effort set to ${value}` : "Reasoning effort cleared",
|
|
104
|
+
tone: "notice",
|
|
105
|
+
data: { effort: value },
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
async stats(_args, ctx, host) {
|
|
109
|
+
const session = host.store.get(ctx.sessionId);
|
|
110
|
+
const runs = host.store.runs?.(ctx.sessionId) ?? [];
|
|
111
|
+
const tokens = runs.reduce((sum, r) => ({
|
|
112
|
+
input: sum.input + (r.usage?.input ?? 0),
|
|
113
|
+
output: sum.output + (r.usage?.output ?? 0),
|
|
114
|
+
}), { input: 0, output: 0 });
|
|
115
|
+
return {
|
|
116
|
+
text: [
|
|
117
|
+
"**Session statistics**",
|
|
118
|
+
"",
|
|
119
|
+
`- Session: \`${session.id}\``,
|
|
120
|
+
`- Model: \`${session.model}\``,
|
|
121
|
+
`- Messages: ${host.store.messages(session.id).length}`,
|
|
122
|
+
`- Runs: ${runs.length}`,
|
|
123
|
+
`- Tokens: in ${tokens.input} · out ${tokens.output}`,
|
|
124
|
+
].join("\n"),
|
|
125
|
+
data: { session, runs: runs.length, tokens },
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
async skills(_args, _ctx, host) {
|
|
129
|
+
const skills = host.skillCatalog?.() ?? [];
|
|
130
|
+
if (!skills.length)
|
|
131
|
+
return { text: "No skills found", tone: "notice" };
|
|
132
|
+
const state = (s) => s.effective ? "enabled" : s.shadowedBy ? `shadowed by ${s.shadowedBy}` : "disabled";
|
|
133
|
+
return {
|
|
134
|
+
text: `**Skills**\n\n| Skill | Scope | State |\n| --- | --- | --- |\n${skills
|
|
135
|
+
.map((s) => `| \`${s.displayId}\` | ${s.scope} | ${state(s)} |`)
|
|
136
|
+
.join("\n")}`,
|
|
137
|
+
data: skills,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
async plugins(_args, _ctx, host) {
|
|
141
|
+
const plugins = host.pluginCatalog?.() ?? [];
|
|
142
|
+
if (!plugins.length)
|
|
143
|
+
return { text: "No plugins loaded", tone: "notice" };
|
|
144
|
+
return {
|
|
145
|
+
text: `**Plugins**\n\n| Plugin | Status | Enabled |\n| --- | --- | --- |\n${plugins
|
|
146
|
+
.map((p) => `| \`${p.id}\` | ${p.status} | ${p.enabled ? "yes" : "no"} |`)
|
|
147
|
+
.join("\n")}`,
|
|
148
|
+
data: plugins,
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
async mcp(_args, _ctx, host) {
|
|
152
|
+
const servers = host.mcp?.list() ?? [];
|
|
153
|
+
if (!servers.length)
|
|
154
|
+
return { text: "No MCP servers configured", tone: "notice" };
|
|
155
|
+
return {
|
|
156
|
+
text: `**MCP servers**\n\n| Server | Status | Tools |\n| --- | --- | --- |\n${servers
|
|
157
|
+
.map((s) => `| \`${s.name}\` | ${s.status} | ${s.counts.tools} |`)
|
|
158
|
+
.join("\n")}`,
|
|
159
|
+
data: servers,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
async agents(args, ctx, host) {
|
|
163
|
+
// With arguments, `/agents <verb>` belongs to the subagents plugin's own command (as in the
|
|
164
|
+
// TUI); without them it lists the active-agent catalog.
|
|
165
|
+
const plugin = args.trim() ? host.plugins?.commands.get("agents") : undefined;
|
|
166
|
+
if (plugin)
|
|
167
|
+
return { text: await plugin(args.trim(), { sessionId: ctx.sessionId }) };
|
|
168
|
+
let state;
|
|
169
|
+
try {
|
|
170
|
+
state = host.plugins?.pluginState?.("subagents", "mainAgents");
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* agent contributions are best-effort */
|
|
174
|
+
}
|
|
175
|
+
const agents = agentCatalogFromState(state);
|
|
176
|
+
const active = resolveActiveAgent(agents, host.config?.agents.active);
|
|
177
|
+
return {
|
|
178
|
+
text: [
|
|
179
|
+
"**Agents**",
|
|
180
|
+
"",
|
|
181
|
+
...agents.map((a) => `- **${a.name}**${a.id === active.id ? " (current)" : ""}${a.readOnly ? " · read-only" : ""} — ${a.description}`),
|
|
182
|
+
].join("\n"),
|
|
183
|
+
data: { agents, active: active.id },
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
export class CommandCatalog {
|
|
188
|
+
host;
|
|
189
|
+
constructor(host) {
|
|
190
|
+
this.host = host;
|
|
191
|
+
}
|
|
192
|
+
builtins() {
|
|
193
|
+
return BUILTIN_COMMANDS.map(builtinDescriptor);
|
|
194
|
+
}
|
|
195
|
+
/** Every descriptor (optionally for one surface). Precedence: builtin > plugin > prompt > skill. */
|
|
196
|
+
list(surface) {
|
|
197
|
+
const out = this.builtins();
|
|
198
|
+
const taken = new Set(out.flatMap((c) => [c.name, ...(c.aliases ?? [])]));
|
|
199
|
+
const push = (descriptor) => {
|
|
200
|
+
if (taken.has(descriptor.name))
|
|
201
|
+
return;
|
|
202
|
+
taken.add(descriptor.name);
|
|
203
|
+
out.push(descriptor);
|
|
204
|
+
};
|
|
205
|
+
const host = this.host;
|
|
206
|
+
if (host?.plugins)
|
|
207
|
+
for (const [name, info] of host.plugins.commandInfo)
|
|
208
|
+
push({
|
|
209
|
+
name,
|
|
210
|
+
description: info.description ?? `plugin ${info.plugin}`,
|
|
211
|
+
...(info.argumentHint ? { argumentHint: info.argumentHint } : {}),
|
|
212
|
+
source: "plugin",
|
|
213
|
+
owner: info.plugin,
|
|
214
|
+
surfaces: [...ALL],
|
|
215
|
+
execution: "core",
|
|
216
|
+
});
|
|
217
|
+
if (host?.prompts)
|
|
218
|
+
for (const template of host.prompts.templates.values())
|
|
219
|
+
push({
|
|
220
|
+
name: template.name,
|
|
221
|
+
description: template.description,
|
|
222
|
+
...(template.argumentHint ? { argumentHint: template.argumentHint } : {}),
|
|
223
|
+
source: "prompt",
|
|
224
|
+
owner: template.source,
|
|
225
|
+
surfaces: [...ALL],
|
|
226
|
+
execution: "surface",
|
|
227
|
+
});
|
|
228
|
+
for (const skill of host?.skillCatalog?.() ?? [])
|
|
229
|
+
if (skill.effective && skill.enabled)
|
|
230
|
+
push({
|
|
231
|
+
name: `skill:${skill.id}`,
|
|
232
|
+
description: skill.description,
|
|
233
|
+
source: "skill",
|
|
234
|
+
...(skill.owner ? { owner: skill.owner.id } : {}),
|
|
235
|
+
surfaces: [...ALL],
|
|
236
|
+
execution: "surface",
|
|
237
|
+
});
|
|
238
|
+
return surface ? out.filter((c) => c.surfaces.includes(surface)) : out;
|
|
239
|
+
}
|
|
240
|
+
/** A command by name or alias (built-in names and aliases are case-insensitive). */
|
|
241
|
+
resolve(name) {
|
|
242
|
+
const lower = name.toLowerCase();
|
|
243
|
+
const all = this.list();
|
|
244
|
+
return (all.find((c) => c.source === "builtin" && (c.name === lower || c.aliases?.includes(lower))) ??
|
|
245
|
+
all.find((c) => c.source !== "builtin" && c.name === name));
|
|
246
|
+
}
|
|
247
|
+
/** Runs an `execution: "core"` command. Surface commands and unknown names throw. */
|
|
248
|
+
async execute(name, args, ctx) {
|
|
249
|
+
const descriptor = this.resolve(name);
|
|
250
|
+
if (!descriptor)
|
|
251
|
+
throw new Error(`Unknown command /${name}. Type /help.`);
|
|
252
|
+
if (descriptor.execution !== "core")
|
|
253
|
+
throw new Error(`/${descriptor.name} is handled by each surface, not by the core catalog`);
|
|
254
|
+
const host = this.host;
|
|
255
|
+
if (!host)
|
|
256
|
+
throw new Error(`/${descriptor.name} needs an application host`);
|
|
257
|
+
if (descriptor.source === "plugin") {
|
|
258
|
+
const handler = host.plugins?.commands.get(descriptor.name);
|
|
259
|
+
if (!handler)
|
|
260
|
+
throw new Error(`Unknown plugin command: ${descriptor.name}`);
|
|
261
|
+
return { text: await handler(args, { sessionId: ctx.sessionId }) };
|
|
262
|
+
}
|
|
263
|
+
const handler = HANDLERS[descriptor.name];
|
|
264
|
+
if (!handler)
|
|
265
|
+
throw new Error(`/${descriptor.name} has no core handler`);
|
|
266
|
+
return handler(args, ctx, host);
|
|
267
|
+
}
|
|
268
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
36
36
|
}, z.core.$strict>>;
|
|
37
37
|
plugins: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
38
38
|
skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
39
|
+
additionalDirectories: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
39
40
|
skillOverrides: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
40
41
|
enabled: z.ZodBoolean;
|
|
41
42
|
}, z.core.$strict>>>;
|
|
@@ -175,6 +176,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
175
176
|
}, z.core.$strip>>;
|
|
176
177
|
tui: z.ZodDefault<z.ZodObject<{
|
|
177
178
|
paddingX: z.ZodDefault<z.ZodNumber>;
|
|
179
|
+
contentPaddingX: z.ZodDefault<z.ZodNumber>;
|
|
178
180
|
skillSlashCommands: z.ZodDefault<z.ZodBoolean>;
|
|
179
181
|
}, z.core.$strict>>;
|
|
180
182
|
agents: z.ZodDefault<z.ZodObject<{
|
|
@@ -195,6 +197,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
195
197
|
};
|
|
196
198
|
plugins: string[];
|
|
197
199
|
skills: string[];
|
|
200
|
+
additionalDirectories: string[];
|
|
198
201
|
skillOverrides: Record<string, {
|
|
199
202
|
enabled: boolean;
|
|
200
203
|
}>;
|
|
@@ -234,6 +237,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
234
237
|
};
|
|
235
238
|
tui: {
|
|
236
239
|
paddingX: number;
|
|
240
|
+
contentPaddingX: number;
|
|
237
241
|
skillSlashCommands: boolean;
|
|
238
242
|
};
|
|
239
243
|
agents: {
|
|
@@ -284,6 +288,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
284
288
|
};
|
|
285
289
|
plugins: string[];
|
|
286
290
|
skills: string[];
|
|
291
|
+
additionalDirectories: string[];
|
|
287
292
|
skillOverrides: Record<string, {
|
|
288
293
|
enabled: boolean;
|
|
289
294
|
}>;
|
|
@@ -376,6 +381,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
376
381
|
};
|
|
377
382
|
tui: {
|
|
378
383
|
paddingX: number;
|
|
384
|
+
contentPaddingX: number;
|
|
379
385
|
skillSlashCommands: boolean;
|
|
380
386
|
};
|
|
381
387
|
agents: {
|
|
@@ -454,6 +460,7 @@ declare const SETTABLE_KEYS: {
|
|
|
454
460
|
readonly "limits.timeoutMs": z.ZodDefault<z.ZodNumber>;
|
|
455
461
|
readonly "pluginHooks.timeoutMs": z.ZodDefault<z.ZodNumber>;
|
|
456
462
|
readonly "tui.paddingX": z.ZodDefault<z.ZodNumber>;
|
|
463
|
+
readonly "tui.contentPaddingX": z.ZodDefault<z.ZodNumber>;
|
|
457
464
|
readonly "tui.skillSlashCommands": z.ZodDefault<z.ZodBoolean>;
|
|
458
465
|
readonly "websearch.provider": z.ZodOptional<z.ZodEnum<{
|
|
459
466
|
brave: "brave";
|
|
@@ -469,6 +476,13 @@ declare const SETTABLE_KEYS: {
|
|
|
469
476
|
};
|
|
470
477
|
export type SettableSettingKey = keyof typeof SETTABLE_KEYS;
|
|
471
478
|
export declare function isSettableSettingKey(key: string): key is SettableSettingKey;
|
|
479
|
+
export interface SettableSettingInfo {
|
|
480
|
+
key: SettableSettingKey;
|
|
481
|
+
kind: "boolean" | "number" | "string" | "enum";
|
|
482
|
+
options?: string[];
|
|
483
|
+
}
|
|
484
|
+
/** Every settable key with its value kind (and enum options), for settings editors. */
|
|
485
|
+
export declare function settableSettings(): SettableSettingInfo[];
|
|
472
486
|
/**
|
|
473
487
|
* Atomically sets one user-facing setting in the global config file (`<config home>/config.json`)
|
|
474
488
|
* while preserving every unrelated JSON field. The value is validated against the same zod leaf
|
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";
|
|
@@ -81,6 +81,12 @@ const configObjectSchema = z
|
|
|
81
81
|
provider: providerSchema.default(() => providerSchema.parse({})),
|
|
82
82
|
plugins: z.array(z.string()).default([]),
|
|
83
83
|
skills: z.array(z.string()).default([]),
|
|
84
|
+
/**
|
|
85
|
+
* Extra directories the mediated path policy may touch outside the workspace. Resolved
|
|
86
|
+
* relative to the defining configuration file and canonicalized on load. Additive across
|
|
87
|
+
* layers like `plugins`/`skills`: a lower layer can only ADD, never clear the global list.
|
|
88
|
+
*/
|
|
89
|
+
additionalDirectories: z.array(z.string().trim().min(1)).default([]),
|
|
84
90
|
/** Project-local enable/disable overrides for effective non-plugin skills. */
|
|
85
91
|
skillOverrides: z.record(z.string(), z.object({ enabled: z.boolean() }).strict()).default({}),
|
|
86
92
|
mcp: z
|
|
@@ -178,8 +184,15 @@ const configObjectSchema = z
|
|
|
178
184
|
})),
|
|
179
185
|
tui: z
|
|
180
186
|
.object({
|
|
181
|
-
/** Horizontal padding (columns) around the editor input box. */
|
|
187
|
+
/** Horizontal padding (columns) around the editor input box. Editor-only. */
|
|
182
188
|
paddingX: z.number().int().min(0).max(4).default(1),
|
|
189
|
+
/**
|
|
190
|
+
* Horizontal inset (columns) applied to each side of the transcript content, so
|
|
191
|
+
* agent-written rows keep breathing room from the window borders. Independent of
|
|
192
|
+
* `paddingX` (which styles only the editor input box). Clamped on narrow terminals so the
|
|
193
|
+
* content column never collapses below a usable width.
|
|
194
|
+
*/
|
|
195
|
+
contentPaddingX: z.number().int().min(0).max(12).default(2),
|
|
183
196
|
/**
|
|
184
197
|
* Offer effective skills as first-class `skill:<id>` editor slash-autocomplete entries.
|
|
185
198
|
* Off hides those entries; the `/skills` manager and its argument completion stay available.
|
|
@@ -187,7 +200,7 @@ const configObjectSchema = z
|
|
|
187
200
|
skillSlashCommands: z.boolean().default(true),
|
|
188
201
|
})
|
|
189
202
|
.strict()
|
|
190
|
-
.default(() => ({ paddingX: 1, skillSlashCommands: true })),
|
|
203
|
+
.default(() => ({ paddingX: 1, contentPaddingX: 2, skillSlashCommands: true })),
|
|
191
204
|
/**
|
|
192
205
|
* The ACTIVE agent drives the main session: its system prompt is appended to every prompt and
|
|
193
206
|
* its optional model selector is offered when the agent is chosen (`/agents`). `build` is the
|
|
@@ -237,6 +250,26 @@ function isUsableLegacyModel(model) {
|
|
|
237
250
|
const normalized = model.trim();
|
|
238
251
|
return normalized !== "" && normalized !== LEGACY_PROVIDER_MODEL_PLACEHOLDER;
|
|
239
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Appends the entries of `selected` after the `global` list, dropping exact duplicates so the
|
|
255
|
+
* global entries keep their order and the selected layer only ever ADDS to the collection. An
|
|
256
|
+
* empty `selected` list therefore leaves the global list intact. Entries were already resolved
|
|
257
|
+
* per layer by `parseLayer`, so deduplication happens on the resolved strings.
|
|
258
|
+
*/
|
|
259
|
+
function mergeUnique(global, selected) {
|
|
260
|
+
const seen = new Set(global);
|
|
261
|
+
const merged = [...global];
|
|
262
|
+
for (const entry of selected)
|
|
263
|
+
if (!seen.has(entry)) {
|
|
264
|
+
seen.add(entry);
|
|
265
|
+
merged.push(entry);
|
|
266
|
+
}
|
|
267
|
+
return merged;
|
|
268
|
+
}
|
|
269
|
+
/** Absolute, canonical directory when it exists; a missing path keeps its resolved absolute form. */
|
|
270
|
+
async function canonicalDirectory(path) {
|
|
271
|
+
return realpath(path).catch(() => resolve(path));
|
|
272
|
+
}
|
|
240
273
|
/**
|
|
241
274
|
* Whether legacy provider selection should take priority over a saved plugin profile for this
|
|
242
275
|
* run: an explicit endpoint override, or a trusted project/explicit layer whose parsed root
|
|
@@ -269,6 +302,9 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
|
269
302
|
const raw = (await readJson(file)) ?? {};
|
|
270
303
|
const config = configSchema.parse(raw);
|
|
271
304
|
config.skills = config.skills.map((p) => resolve(file, "..", p));
|
|
305
|
+
// Extra tool roots are paths relative to the config file; canonicalize so containment checks
|
|
306
|
+
// compare real directories (a symlinked root never escapes its declared location).
|
|
307
|
+
config.additionalDirectories = await Promise.all(config.additionalDirectories.map((p) => canonicalDirectory(resolve(file, "..", p))));
|
|
272
308
|
// Plugin entries are paths (relative to the config file) or npm package names.
|
|
273
309
|
config.plugins = config.plugins.map((p) => (isPathSpec(p) ? resolve(file, "..", p) : p));
|
|
274
310
|
for (const server of Object.values(config.mcp.servers))
|
|
@@ -310,8 +346,44 @@ export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
|
310
346
|
for (const key of selected.keys) {
|
|
311
347
|
if (key === "mcp" || key === "mcpServers")
|
|
312
348
|
continue;
|
|
313
|
-
if (key in selected.config)
|
|
314
|
-
|
|
349
|
+
if (!(key in selected.config))
|
|
350
|
+
continue;
|
|
351
|
+
// Additive collections: the selected layer ADDS to the global list (global entries
|
|
352
|
+
// first, exact duplicates dropped); an empty selected list never clears the global one.
|
|
353
|
+
if (key === "plugins") {
|
|
354
|
+
overlaid.plugins = mergeUnique(overlaid.plugins, selected.config.plugins);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (key === "skills") {
|
|
358
|
+
overlaid.skills = mergeUnique(overlaid.skills, selected.config.skills);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (key === "additionalDirectories") {
|
|
362
|
+
overlaid.additionalDirectories = mergeUnique(overlaid.additionalDirectories, selected.config.additionalDirectories);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
// Additive records: merge by key, the selected layer wins per key.
|
|
366
|
+
if (key === "pluginOverrides") {
|
|
367
|
+
overlaid.pluginOverrides = {
|
|
368
|
+
...overlaid.pluginOverrides,
|
|
369
|
+
...selected.config.pluginOverrides,
|
|
370
|
+
};
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (key === "skillOverrides") {
|
|
374
|
+
// Global skill overrides were reset above (skill activation is project-local); the
|
|
375
|
+
// selected overrides still merge into that empty record and win per key.
|
|
376
|
+
overlaid.skillOverrides = { ...overlaid.skillOverrides, ...selected.config.skillOverrides };
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (key === "builtinPlugins") {
|
|
380
|
+
// Per-plugin options merge by plugin id: the selected entry wins per id, so
|
|
381
|
+
// `{id: {enabled: false}}` in the selected layer still disables that built-in while
|
|
382
|
+
// every other global entry stays.
|
|
383
|
+
overlaid.builtinPlugins = { ...overlaid.builtinPlugins, ...selected.config.builtinPlugins };
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
overlaid[key] = selected.config[key];
|
|
315
387
|
}
|
|
316
388
|
if (selected.keys.has("mcp") || selected.keys.has("mcpServers"))
|
|
317
389
|
// Only servers merge upward; `mcp.allow` is a global/user preference and is deliberately
|
|
@@ -470,6 +542,7 @@ const SETTABLE_KEYS = {
|
|
|
470
542
|
"limits.timeoutMs": SETTABLE_SECTIONS.limits.shape.timeoutMs,
|
|
471
543
|
"pluginHooks.timeoutMs": SETTABLE_SECTIONS.pluginHooks.shape.timeoutMs,
|
|
472
544
|
"tui.paddingX": SETTABLE_SECTIONS.tui.shape.paddingX,
|
|
545
|
+
"tui.contentPaddingX": SETTABLE_SECTIONS.tui.shape.contentPaddingX,
|
|
473
546
|
"tui.skillSlashCommands": SETTABLE_SECTIONS.tui.shape.skillSlashCommands,
|
|
474
547
|
"websearch.provider": SETTABLE_SECTIONS.websearch.shape.provider,
|
|
475
548
|
"agents.active": SETTABLE_SECTIONS.agents.shape.active,
|
|
@@ -478,6 +551,33 @@ const SETTABLE_KEYS = {
|
|
|
478
551
|
export function isSettableSettingKey(key) {
|
|
479
552
|
return Object.prototype.hasOwnProperty.call(SETTABLE_KEYS, key);
|
|
480
553
|
}
|
|
554
|
+
/** Unwraps optional/default/nullable wrappers down to the value schema. */
|
|
555
|
+
function settingLeaf(schema) {
|
|
556
|
+
let current = schema;
|
|
557
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
558
|
+
const type = current.def?.type;
|
|
559
|
+
if ((type === "optional" || type === "default" || type === "nullable") &&
|
|
560
|
+
current.def?.innerType)
|
|
561
|
+
current = current.def.innerType;
|
|
562
|
+
else
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
return current;
|
|
566
|
+
}
|
|
567
|
+
/** Every settable key with its value kind (and enum options), for settings editors. */
|
|
568
|
+
export function settableSettings() {
|
|
569
|
+
return Object.keys(SETTABLE_KEYS).map((key) => {
|
|
570
|
+
const leaf = settingLeaf(SETTABLE_KEYS[key]);
|
|
571
|
+
const type = leaf.def?.type;
|
|
572
|
+
if (type === "enum")
|
|
573
|
+
return { key, kind: "enum", options: (leaf.options ?? []).map((o) => String(o)) };
|
|
574
|
+
if (type === "boolean")
|
|
575
|
+
return { key, kind: "boolean" };
|
|
576
|
+
if (type === "number" || type === "int")
|
|
577
|
+
return { key, kind: "number" };
|
|
578
|
+
return { key, kind: "string" };
|
|
579
|
+
});
|
|
580
|
+
}
|
|
481
581
|
/**
|
|
482
582
|
* Atomically sets one user-facing setting in the global config file (`<config home>/config.json`)
|
|
483
583
|
* while preserving every unrelated JSON field. The value is validated against the same zod leaf
|
package/dist/core/contracts.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface Session {
|
|
|
18
18
|
options?: Record<string, unknown>;
|
|
19
19
|
createdAt?: number;
|
|
20
20
|
updatedAt?: number;
|
|
21
|
+
/** Root sessions pinned in web clients (v4). */
|
|
22
|
+
pinned?: boolean;
|
|
23
|
+
/** When a root session was archived in web clients (v4); absent when not archived. */
|
|
24
|
+
archivedAt?: number;
|
|
21
25
|
}
|
|
22
26
|
export interface ChildSessionRecord {
|
|
23
27
|
id?: string;
|
|
@@ -30,6 +34,82 @@ export interface ChildSessionRecord {
|
|
|
30
34
|
depth: number;
|
|
31
35
|
options: Record<string, unknown>;
|
|
32
36
|
}
|
|
37
|
+
/** `queued` and `running` are the only non-terminal run states. */
|
|
38
|
+
export type RunStatus = "queued" | "running" | "completed" | "turns_exceeded" | "failed" | "cancelled" | "interrupted";
|
|
39
|
+
export type TerminalRunStatus = Exclude<RunStatus, "queued" | "running">;
|
|
40
|
+
/** One `AgentRunner.run` execution (v4 `runs` table). `id` equals `RunEvent.runId`. */
|
|
41
|
+
export interface RunRecord {
|
|
42
|
+
id: string;
|
|
43
|
+
session: string;
|
|
44
|
+
status: RunStatus;
|
|
45
|
+
/** Client idempotency key; absent for TUI/headless runs. */
|
|
46
|
+
requestId?: string;
|
|
47
|
+
correlationId?: string;
|
|
48
|
+
/** Process that executes (or executed) the run, for startup reconciliation. */
|
|
49
|
+
ownerPid?: number;
|
|
50
|
+
model?: string;
|
|
51
|
+
createdAt: number;
|
|
52
|
+
startedAt?: number;
|
|
53
|
+
endedAt?: number;
|
|
54
|
+
error?: string;
|
|
55
|
+
usage?: {
|
|
56
|
+
input: number;
|
|
57
|
+
output: number;
|
|
58
|
+
cachedInput?: number;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export interface BeginRunInput {
|
|
62
|
+
id: string;
|
|
63
|
+
session: string;
|
|
64
|
+
/** `queued` for scheduled runs, `running` (default) when execution starts now. */
|
|
65
|
+
status?: "queued" | "running";
|
|
66
|
+
requestId?: string;
|
|
67
|
+
correlationId?: string;
|
|
68
|
+
model?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface EndRunInput {
|
|
71
|
+
status: TerminalRunStatus;
|
|
72
|
+
error?: string;
|
|
73
|
+
usage?: {
|
|
74
|
+
input: number;
|
|
75
|
+
output: number;
|
|
76
|
+
cachedInput?: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export interface PageOptions {
|
|
80
|
+
/** Only rows with a sequence greater than this (ascending from there). */
|
|
81
|
+
after?: number;
|
|
82
|
+
/** Only rows with a sequence lower than this (the newest `limit` of them). */
|
|
83
|
+
before?: number;
|
|
84
|
+
limit?: number;
|
|
85
|
+
}
|
|
86
|
+
export interface MessagePage {
|
|
87
|
+
items: Array<{
|
|
88
|
+
seq: number;
|
|
89
|
+
message: Message;
|
|
90
|
+
compacted: boolean;
|
|
91
|
+
}>;
|
|
92
|
+
/** More rows exist beyond the page in the paging direction. */
|
|
93
|
+
hasMore: boolean;
|
|
94
|
+
}
|
|
95
|
+
export interface StoredEvent {
|
|
96
|
+
/** Global `events.seq` as a string (same value as `RunEvent.eventId`). */
|
|
97
|
+
eventId: string;
|
|
98
|
+
runId: string;
|
|
99
|
+
type: string;
|
|
100
|
+
data: unknown;
|
|
101
|
+
createdAt?: number;
|
|
102
|
+
correlationId?: string;
|
|
103
|
+
}
|
|
104
|
+
export interface EventPage {
|
|
105
|
+
items: StoredEvent[];
|
|
106
|
+
hasMore: boolean;
|
|
107
|
+
}
|
|
108
|
+
/** Optional tool-call metadata (v4 columns); stores may ignore it. */
|
|
109
|
+
export interface ToolCallMeta {
|
|
110
|
+
runId?: string;
|
|
111
|
+
effect?: string;
|
|
112
|
+
}
|
|
33
113
|
export interface SessionStore {
|
|
34
114
|
create(workspace: string, provider: string, model: string): Session;
|
|
35
115
|
get(id: string): Session;
|
|
@@ -64,12 +144,39 @@ export interface SessionStore {
|
|
|
64
144
|
overwrite(id: string, messages: Message[]): void;
|
|
65
145
|
acquire(id: string): void;
|
|
66
146
|
release(id: string): void;
|
|
67
|
-
beginCall(session: string, call: ToolCall): void;
|
|
147
|
+
beginCall(session: string, call: ToolCall, meta?: ToolCallMeta): void;
|
|
68
148
|
endCall(session: string, call: ToolCall, result: ToolResult): void;
|
|
69
149
|
/** The persisted result of a completed tool call, for replay of rich (ui/image) parts. */
|
|
70
150
|
callResult(session: string, callId: string): ToolResult | undefined;
|
|
71
151
|
reconcile(id: string, acknowledge?: boolean): void;
|
|
72
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Persist a durable run event. Stores that assign a global, monotonic sequence return it; the
|
|
154
|
+
* runner exposes it as `RunEvent.eventId`. Returning nothing is allowed (no `eventId`).
|
|
155
|
+
*/
|
|
156
|
+
event(id: string, runId: string, type: string, data: unknown, meta?: {
|
|
157
|
+
correlationId?: string;
|
|
158
|
+
}): number | void;
|
|
159
|
+
/**
|
|
160
|
+
* Records a run, or moves an existing `queued` run with the same id to `running`. A run whose
|
|
161
|
+
* `(session, requestId)` already exists is returned as is (`created: false`): idempotency.
|
|
162
|
+
*/
|
|
163
|
+
beginRun?(input: BeginRunInput): {
|
|
164
|
+
run: RunRecord;
|
|
165
|
+
created: boolean;
|
|
166
|
+
};
|
|
167
|
+
/** Moves a non-terminal run to a terminal state; terminal runs are never overwritten. */
|
|
168
|
+
endRun?(id: string, input: EndRunInput): void;
|
|
169
|
+
runByRequest?(session: string, requestId: string): RunRecord | undefined;
|
|
170
|
+
/** Runs of a session, newest first. */
|
|
171
|
+
runs?(session: string, options?: {
|
|
172
|
+
limit?: number;
|
|
173
|
+
}): RunRecord[];
|
|
174
|
+
messagesPage?(session: string, options?: PageOptions & {
|
|
175
|
+
compacted?: boolean;
|
|
176
|
+
}): MessagePage;
|
|
177
|
+
eventsPage?(session: string, options?: PageOptions): EventPage;
|
|
178
|
+
/** Marks queued/running runs whose owner process is not alive as interrupted. */
|
|
179
|
+
interruptRuns?(): number;
|
|
73
180
|
}
|
|
74
181
|
export interface ContextSource {
|
|
75
182
|
/** System instructions; `sessionId` scopes lazily attached nested instructions. */
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Attachment, type Message, type ModelProvider, type RunEvent, type ToolDefinition } from "@alisio/sdk";
|
|
2
|
+
import type { PathAccess } from "../runtime/access.ts";
|
|
2
3
|
import type { ApprovalHandler, ContextSource, Policy, RunnerExtensions, SessionStore } from "./contracts.ts";
|
|
3
4
|
import type { ToolRegistry } from "./registry.ts";
|
|
4
5
|
export interface CompactionSettings {
|
|
@@ -34,6 +35,11 @@ export interface RunnerOptions {
|
|
|
34
35
|
compaction?: CompactionSettings;
|
|
35
36
|
/** When set, write/process tools not allowed by the policy are offered and ask first. */
|
|
36
37
|
approve?: ApprovalHandler;
|
|
38
|
+
/**
|
|
39
|
+
* Mediated path policy (workspace plus declared extra directories and external approval).
|
|
40
|
+
* When set, declared tool paths outside the allowed roots ask before the effect gate runs.
|
|
41
|
+
*/
|
|
42
|
+
pathAccess?: PathAccess;
|
|
37
43
|
/** Generic hooks (implemented by the plugin host) for compaction and session start. */
|
|
38
44
|
extensions?: RunnerExtensions;
|
|
39
45
|
/**
|
|
@@ -87,6 +93,10 @@ export interface RunOptions {
|
|
|
87
93
|
timeoutMs?: number;
|
|
88
94
|
/** Reasoning effort level for this run; beats the runner-level default when set. */
|
|
89
95
|
reasoningEffort?: string;
|
|
96
|
+
/** Run id to use for this run's events (e.g. preassigned by an embedder); a UUID otherwise. */
|
|
97
|
+
runId?: string;
|
|
98
|
+
/** Copied to every event of this run as `RunEvent.correlationId` (e.g. an HTTP request id). */
|
|
99
|
+
correlationId?: string;
|
|
90
100
|
}
|
|
91
101
|
/**
|
|
92
102
|
* Default cumulative token budget for one run: several context windows (each turn re-sends the
|