@alisio/core 0.1.0-alpha.17 → 0.1.0-alpha.19

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.
@@ -0,0 +1,292 @@
1
+ import { agentCatalogFromState, resolveActiveAgent } from "../agents/active.js";
2
+ import { formatSideQuestion, SIDE_QUESTION_DESCRIPTION, SIDE_QUESTION_USAGE, } from "../sessions/side-questions.js";
3
+ import { BUILTIN_COMMANDS } from "./builtins.js";
4
+ export { BUILTIN_COMMANDS };
5
+ const builtinDescriptor = (c) => ({
6
+ name: c.name,
7
+ description: c.description,
8
+ ...(c.aliases ? { aliases: [...c.aliases] } : {}),
9
+ ...(c.argumentHint ? { argumentHint: c.argumentHint } : {}),
10
+ source: "builtin",
11
+ surfaces: [...c.surfaces],
12
+ execution: c.execution,
13
+ });
14
+ const ALL = ["tui", "web", "api"];
15
+ /** Root sessions of this workspace bound to the current provider, newest first (as the TUI). */
16
+ const workspaceSessions = (host) => host.store
17
+ .list()
18
+ .filter((s) => s.workspace === host.workspace && s.provider === host.provider.id);
19
+ const firstPrompt = (host, id) => {
20
+ const first = host.store.messages(id).find((m) => m.role === "user" && !m.summary);
21
+ return first?.role === "user" ? first.text.replace(/\s+/g, " ").slice(0, 60) : "";
22
+ };
23
+ const HANDLERS = {
24
+ async tools(_args, _ctx, host) {
25
+ const policy = host.runner.policy;
26
+ const rows = host.registry
27
+ .list()
28
+ .map((t) => {
29
+ const effect = t.effect ?? "external";
30
+ const state = effect === "read" || effect === "internal" || policy[effect]
31
+ ? "enabled"
32
+ : host.runner.approvals && (effect === "write" || effect === "process")
33
+ ? "ask"
34
+ : "disabled";
35
+ return `| \`${t.name}\` | ${effect} | ${state} |`;
36
+ })
37
+ .join("\n");
38
+ return {
39
+ 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.`,
40
+ };
41
+ },
42
+ async sessions(_args, ctx, host) {
43
+ const list = workspaceSessions(host).slice(0, 20);
44
+ if (!list.length)
45
+ return { text: "No sessions in this workspace", tone: "notice" };
46
+ return {
47
+ text: [
48
+ "**Recent sessions** (use `/resume <id-prefix>`)",
49
+ "",
50
+ ...list.map((s) => `- \`${s.id}\` ${s.id === ctx.sessionId ? "**(current)** " : ""}${s.model} · ${host.store.messages(s.id).length} msgs · ${firstPrompt(host, s.id) || "_empty_"}`),
51
+ ].join("\n"),
52
+ data: list,
53
+ };
54
+ },
55
+ async resume(args, _ctx, host) {
56
+ const target = args.trim();
57
+ if (!target)
58
+ throw new Error("Usage: /resume <id>");
59
+ const matches = workspaceSessions(host).filter((s) => s.id.startsWith(target));
60
+ if (matches.length !== 1)
61
+ throw new Error(matches.length
62
+ ? `Ambiguous session prefix: ${target}`
63
+ : `No session in this workspace matches ${target}`);
64
+ const found = matches[0];
65
+ return { text: `Resumed session ${found.id}`, tone: "notice", sessionId: found.id };
66
+ },
67
+ async clear(_args, ctx, host) {
68
+ const model = host.store.get(ctx.sessionId).model;
69
+ const created = host.store.create(host.workspace, host.provider.id, model);
70
+ return { text: `New session ${created.id}`, tone: "notice", sessionId: created.id };
71
+ },
72
+ async compact(args, ctx, host) {
73
+ const result = await host.runner.compact(ctx.sessionId, {
74
+ ...(args.trim() ? { focus: args.trim() } : {}),
75
+ ...(ctx.signal ? { signal: ctx.signal } : {}),
76
+ });
77
+ return result
78
+ ? {
79
+ text: `Compacted ${result.replaced} messages: ~${result.before} → ~${result.after} tokens`,
80
+ tone: "notice",
81
+ data: result,
82
+ }
83
+ : { text: "Not enough history to compact", tone: "notice" };
84
+ },
85
+ async model(args, ctx, host) {
86
+ const model = args.trim();
87
+ if (!model)
88
+ throw new Error("Usage: /model <model id>");
89
+ host.runner.setModel(ctx.sessionId, model);
90
+ return { text: `Model set to \`${model}\``, tone: "notice", data: { model } };
91
+ },
92
+ async effort(args, _ctx, host) {
93
+ const level = args.trim();
94
+ if (!level)
95
+ return {
96
+ text: `Reasoning effort: ${host.config?.agents.effort ?? "model default"}`,
97
+ tone: "notice",
98
+ };
99
+ if (!host.updateSetting)
100
+ throw new Error("Effort changes are unavailable here");
101
+ const value = level === "!clear" ? undefined : level;
102
+ await host.updateSetting("agents.effort", value);
103
+ return {
104
+ text: value ? `Reasoning effort set to ${value}` : "Reasoning effort cleared",
105
+ tone: "notice",
106
+ data: { effort: value },
107
+ };
108
+ },
109
+ async stats(_args, ctx, host) {
110
+ const session = host.store.get(ctx.sessionId);
111
+ const runs = host.store.runs?.(ctx.sessionId) ?? [];
112
+ const tokens = runs.reduce((sum, r) => ({
113
+ input: sum.input + (r.usage?.input ?? 0),
114
+ output: sum.output + (r.usage?.output ?? 0),
115
+ }), { input: 0, output: 0 });
116
+ return {
117
+ text: [
118
+ "**Session statistics**",
119
+ "",
120
+ `- Session: \`${session.id}\``,
121
+ `- Model: \`${session.model}\``,
122
+ `- Messages: ${host.store.messages(session.id).length}`,
123
+ `- Runs: ${runs.length}`,
124
+ `- Tokens: in ${tokens.input} · out ${tokens.output}`,
125
+ ].join("\n"),
126
+ data: { session, runs: runs.length, tokens },
127
+ };
128
+ },
129
+ async skills(_args, _ctx, host) {
130
+ const skills = host.skillCatalog?.() ?? [];
131
+ if (!skills.length)
132
+ return { text: "No skills found", tone: "notice" };
133
+ const state = (s) => s.effective ? "enabled" : s.shadowedBy ? `shadowed by ${s.shadowedBy}` : "disabled";
134
+ return {
135
+ text: `**Skills**\n\n| Skill | Scope | State |\n| --- | --- | --- |\n${skills
136
+ .map((s) => `| \`${s.displayId}\` | ${s.scope} | ${state(s)} |`)
137
+ .join("\n")}`,
138
+ data: skills,
139
+ };
140
+ },
141
+ async plugins(_args, _ctx, host) {
142
+ const plugins = host.pluginCatalog?.() ?? [];
143
+ if (!plugins.length)
144
+ return { text: "No plugins loaded", tone: "notice" };
145
+ return {
146
+ text: `**Plugins**\n\n| Plugin | Status | Enabled |\n| --- | --- | --- |\n${plugins
147
+ .map((p) => `| \`${p.id}\` | ${p.status} | ${p.enabled ? "yes" : "no"} |`)
148
+ .join("\n")}`,
149
+ data: plugins,
150
+ };
151
+ },
152
+ async mcp(_args, _ctx, host) {
153
+ const servers = host.mcp?.list() ?? [];
154
+ if (!servers.length)
155
+ return { text: "No MCP servers configured", tone: "notice" };
156
+ return {
157
+ text: `**MCP servers**\n\n| Server | Status | Tools |\n| --- | --- | --- |\n${servers
158
+ .map((s) => `| \`${s.name}\` | ${s.status} | ${s.counts.tools} |`)
159
+ .join("\n")}`,
160
+ data: servers,
161
+ };
162
+ },
163
+ async btw(args, ctx, host) {
164
+ const question = args.trim();
165
+ const side = host.sideQuestions;
166
+ if (!question) {
167
+ const history = side?.history(ctx.sessionId) ?? [];
168
+ const latest = history.at(-1);
169
+ if (!latest)
170
+ return {
171
+ text: `${SIDE_QUESTION_USAGE}\n${SIDE_QUESTION_DESCRIPTION}`,
172
+ tone: "notice",
173
+ data: { history },
174
+ };
175
+ return {
176
+ text: formatSideQuestion(latest, history.length, history.length),
177
+ data: { history, entry: latest },
178
+ };
179
+ }
180
+ if (!side)
181
+ throw new Error("Side questions are unavailable here");
182
+ const entry = await side.ask(ctx.sessionId, question, ctx.signal ? { signal: ctx.signal } : {});
183
+ const total = side.history(ctx.sessionId).length;
184
+ return { text: formatSideQuestion(entry, total, total), data: { entry } };
185
+ },
186
+ async agents(args, ctx, host) {
187
+ // With arguments, `/agents <verb>` belongs to the subagents plugin's own command (as in the
188
+ // TUI); without them it lists the active-agent catalog.
189
+ const plugin = args.trim() ? host.plugins?.commands.get("agents") : undefined;
190
+ if (plugin)
191
+ return { text: await plugin(args.trim(), { sessionId: ctx.sessionId }) };
192
+ let state;
193
+ try {
194
+ state = host.plugins?.pluginState?.("subagents", "mainAgents");
195
+ }
196
+ catch {
197
+ /* agent contributions are best-effort */
198
+ }
199
+ const agents = agentCatalogFromState(state);
200
+ const active = resolveActiveAgent(agents, host.config?.agents.active);
201
+ return {
202
+ text: [
203
+ "**Agents**",
204
+ "",
205
+ ...agents.map((a) => `- **${a.name}**${a.id === active.id ? " (current)" : ""}${a.readOnly ? " · read-only" : ""} — ${a.description}`),
206
+ ].join("\n"),
207
+ data: { agents, active: active.id },
208
+ };
209
+ },
210
+ };
211
+ export class CommandCatalog {
212
+ host;
213
+ constructor(host) {
214
+ this.host = host;
215
+ }
216
+ builtins() {
217
+ return BUILTIN_COMMANDS.map(builtinDescriptor);
218
+ }
219
+ /** Every descriptor (optionally for one surface). Precedence: builtin > plugin > prompt > skill. */
220
+ list(surface) {
221
+ const out = this.builtins();
222
+ const taken = new Set(out.flatMap((c) => [c.name, ...(c.aliases ?? [])]));
223
+ const push = (descriptor) => {
224
+ if (taken.has(descriptor.name))
225
+ return;
226
+ taken.add(descriptor.name);
227
+ out.push(descriptor);
228
+ };
229
+ const host = this.host;
230
+ if (host?.plugins)
231
+ for (const [name, info] of host.plugins.commandInfo)
232
+ push({
233
+ name,
234
+ description: info.description ?? `plugin ${info.plugin}`,
235
+ ...(info.argumentHint ? { argumentHint: info.argumentHint } : {}),
236
+ source: "plugin",
237
+ owner: info.plugin,
238
+ surfaces: [...ALL],
239
+ execution: "core",
240
+ });
241
+ if (host?.prompts)
242
+ for (const template of host.prompts.templates.values())
243
+ push({
244
+ name: template.name,
245
+ description: template.description,
246
+ ...(template.argumentHint ? { argumentHint: template.argumentHint } : {}),
247
+ source: "prompt",
248
+ owner: template.source,
249
+ surfaces: [...ALL],
250
+ execution: "surface",
251
+ });
252
+ for (const skill of host?.skillCatalog?.() ?? [])
253
+ if (skill.effective && skill.enabled)
254
+ push({
255
+ name: `skill:${skill.id}`,
256
+ description: skill.description,
257
+ source: "skill",
258
+ ...(skill.owner ? { owner: skill.owner.id } : {}),
259
+ surfaces: [...ALL],
260
+ execution: "surface",
261
+ });
262
+ return surface ? out.filter((c) => c.surfaces.includes(surface)) : out;
263
+ }
264
+ /** A command by name or alias (built-in names and aliases are case-insensitive). */
265
+ resolve(name) {
266
+ const lower = name.toLowerCase();
267
+ const all = this.list();
268
+ return (all.find((c) => c.source === "builtin" && (c.name === lower || c.aliases?.includes(lower))) ??
269
+ all.find((c) => c.source !== "builtin" && c.name === name));
270
+ }
271
+ /** Runs an `execution: "core"` command. Surface commands and unknown names throw. */
272
+ async execute(name, args, ctx) {
273
+ const descriptor = this.resolve(name);
274
+ if (!descriptor)
275
+ throw new Error(`Unknown command /${name}. Type /help.`);
276
+ if (descriptor.execution !== "core")
277
+ throw new Error(`/${descriptor.name} is handled by each surface, not by the core catalog`);
278
+ const host = this.host;
279
+ if (!host)
280
+ throw new Error(`/${descriptor.name} needs an application host`);
281
+ if (descriptor.source === "plugin") {
282
+ const handler = host.plugins?.commands.get(descriptor.name);
283
+ if (!handler)
284
+ throw new Error(`Unknown plugin command: ${descriptor.name}`);
285
+ return { text: await handler(args, { sessionId: ctx.sessionId }) };
286
+ }
287
+ const handler = HANDLERS[descriptor.name];
288
+ if (!handler)
289
+ throw new Error(`/${descriptor.name} has no core handler`);
290
+ return handler(args, ctx, host);
291
+ }
292
+ }
package/dist/config.d.ts CHANGED
@@ -476,6 +476,13 @@ declare const SETTABLE_KEYS: {
476
476
  };
477
477
  export type SettableSettingKey = keyof typeof SETTABLE_KEYS;
478
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[];
479
486
  /**
480
487
  * Atomically sets one user-facing setting in the global config file (`<config home>/config.json`)
481
488
  * while preserving every unrelated JSON field. The value is validated against the same zod leaf
package/dist/config.js CHANGED
@@ -551,6 +551,33 @@ const SETTABLE_KEYS = {
551
551
  export function isSettableSettingKey(key) {
552
552
  return Object.prototype.hasOwnProperty.call(SETTABLE_KEYS, key);
553
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
+ }
554
581
  /**
555
582
  * Atomically sets one user-facing setting in the global config file (`<config home>/config.json`)
556
583
  * while preserving every unrelated JSON field. The value is validated against the same zod leaf
@@ -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
- event(id: string, runId: string, type: string, data: unknown): void;
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. */
@@ -93,6 +93,10 @@ export interface RunOptions {
93
93
  timeoutMs?: number;
94
94
  /** Reasoning effort level for this run; beats the runner-level default when set. */
95
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;
96
100
  }
97
101
  /**
98
102
  * Default cumulative token budget for one run: several context windows (each turn re-sends the
@@ -153,6 +157,23 @@ export declare class AgentRunner {
153
157
  basis: "window" | "unknown";
154
158
  compactionAt: number;
155
159
  };
160
+ /**
161
+ * What a tool-less side call about a session needs (for example a `/btw` side question): the
162
+ * session's provider and model, its system instructions, its ACTIVE history and the current
163
+ * limits. Claims no lock and writes nothing, so it is safe while a run is active.
164
+ */
165
+ sideContext(sessionId: string): Promise<{
166
+ provider: ModelProvider;
167
+ model: string;
168
+ instructions: string;
169
+ messages: Message[];
170
+ maxContextChars: number;
171
+ maxOutputTokens: number;
172
+ timeoutMs: number;
173
+ /** Context window of the model in tokens, when known and trusted. */
174
+ contextWindow?: number;
175
+ reasoningEffort?: string;
176
+ }>;
156
177
  /** Change the model used by subsequent turns of a session; recorded in the store. */
157
178
  setModel(sessionId: string, model: string): void;
158
179
  /** Manually compact a session outside a run. */