@alisio/core 0.1.0-alpha.17 → 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.
@@ -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
@@ -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
@@ -1,4 +1,4 @@
1
- import { textProjection, textResult, } from "@alisio/sdk";
1
+ import { isEphemeralRunEventType, textProjection, textResult, } from "@alisio/sdk";
2
2
  import { checkpointInstructions, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, reduceMessagesToBudget, shouldCompactContext, summarize, summaryMessage, } from "./compaction.js";
3
3
  /**
4
4
  * Default cumulative token budget for one run: several context windows (each turn re-sends the
@@ -49,8 +49,9 @@ export class AgentRunner {
49
49
  ...(compaction ? { compaction: { ...this.options.compaction, ...compaction } } : {}),
50
50
  };
51
51
  }
52
- emitter(sessionId) {
53
- const runId = crypto.randomUUID();
52
+ emitter(sessionId, run = {}) {
53
+ const runId = run.runId ?? crypto.randomUUID();
54
+ const correlationId = run.correlationId;
54
55
  let seq = 0;
55
56
  return (type, data) => {
56
57
  const event = {
@@ -62,8 +63,15 @@ export class AgentRunner {
62
63
  timestamp: new Date().toISOString(),
63
64
  data,
64
65
  };
65
- if (type !== "text_delta" && type !== "tool_progress" && type !== "reasoning_delta")
66
- this.options.store.event(sessionId, runId, type, data);
66
+ if (!isEphemeralRunEventType(type)) {
67
+ const persisted = correlationId !== undefined
68
+ ? this.options.store.event(sessionId, runId, type, data, { correlationId })
69
+ : this.options.store.event(sessionId, runId, type, data);
70
+ if (typeof persisted === "number")
71
+ event.eventId = String(persisted);
72
+ }
73
+ if (correlationId !== undefined)
74
+ event.correlationId = correlationId;
67
75
  try {
68
76
  this.options.onEvent?.(event);
69
77
  }
@@ -280,7 +288,17 @@ export class AgentRunner {
280
288
  }
281
289
  async run(sessionId, prompt, signal, options = {}) {
282
290
  const controller = this.claim(sessionId);
283
- const o = this.options, emit = this.emitter(sessionId);
291
+ const runId = options.runId ?? crypto.randomUUID();
292
+ const o = this.options, emit = this.emitter(sessionId, { runId, correlationId: options.correlationId });
293
+ /** Run journal (v4 stores only): an auxiliary record that never changes the run outcome. */
294
+ const finish = (status, error) => {
295
+ try {
296
+ o.store.endRun?.(runId, { status, usage: usageTotal, ...(error ? { error } : {}) });
297
+ }
298
+ catch {
299
+ /* A journal write failure cannot invalidate an executed run. */
300
+ }
301
+ };
284
302
  const limit = o.maxContextChars ?? 800_000;
285
303
  let tokens = 0, lastText = "", acquired = false;
286
304
  const usageTotal = { input: 0, output: 0 };
@@ -296,6 +314,13 @@ export class AgentRunner {
296
314
  return options.instructions ? `${base}\n\n${options.instructions}` : base;
297
315
  };
298
316
  try {
317
+ o.store.beginRun?.({
318
+ id: runId,
319
+ session: sessionId,
320
+ status: "running",
321
+ model: o.store.get(sessionId).model,
322
+ ...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
323
+ });
299
324
  o.store.acquire(sessionId);
300
325
  acquired = true;
301
326
  o.store.reconcile(sessionId);
@@ -414,6 +439,8 @@ export class AgentRunner {
414
439
  // display-only extension of the persisted transcript and never reach a model prompt
415
440
  // (raw image bytes included). The store keeps the rich parts for TUI replay.
416
441
  const providerMessages = messages.map((m) => m.role === "tool" ? { ...m, result: textProjection(m.result) } : m);
442
+ const requested = Date.now();
443
+ let firstDelta;
417
444
  for await (const e of provider.stream({
418
445
  instructions,
419
446
  messages: providerMessages,
@@ -428,6 +455,8 @@ export class AgentRunner {
428
455
  : {}),
429
456
  })) {
430
457
  combined.throwIfAborted();
458
+ if (e.type !== "completed")
459
+ firstDelta ??= Date.now();
431
460
  if (e.type === "text_delta")
432
461
  emit("text_delta", { delta: e.delta });
433
462
  else if (e.type === "reasoning_delta")
@@ -445,6 +474,7 @@ export class AgentRunner {
445
474
  }
446
475
  if (!completion)
447
476
  throw new Error("Provider stream ended without a completed response");
477
+ const durationMs = Date.now() - requested;
448
478
  const ids = completion.calls.map((c) => c.id);
449
479
  const previousIds = new Set(messages.flatMap((m) => (m.role === "assistant" ? m.calls.map((c) => c.id) : [])));
450
480
  if (ids.some((id) => previousIds.has(id)))
@@ -466,6 +496,8 @@ export class AgentRunner {
466
496
  calls: completion.calls.length,
467
497
  model,
468
498
  ...(usage ? { usage } : {}),
499
+ durationMs,
500
+ ...(firstDelta !== undefined ? { ttftMs: firstDelta - requested } : {}),
469
501
  });
470
502
  if (!completion.calls.length) {
471
503
  const final = {
@@ -481,6 +513,7 @@ export class AgentRunner {
481
513
  final.truncated = true;
482
514
  }
483
515
  emit("run_completed", final);
516
+ finish("completed");
484
517
  return { sessionId, text: lastText, status: "completed", usage: usageTotal };
485
518
  }
486
519
  const prepared = completion.calls.map((call) => {
@@ -571,7 +604,7 @@ export class AgentRunner {
571
604
  if (decision === "session")
572
605
  policy[effect] = true;
573
606
  }
574
- o.store.beginCall(sessionId, call);
607
+ o.store.beginCall(sessionId, call, { runId, effect });
575
608
  result = await p.tool.execute(p.input, {
576
609
  signal: combined,
577
610
  workspace,
@@ -594,8 +627,11 @@ export class AgentRunner {
594
627
  }
595
628
  : {}),
596
629
  });
597
- if (JSON.stringify(result).length > 48_000)
598
- result = textResult(`${JSON.stringify(result).slice(0, 40_000)}\n[tool output truncated]`, result.isError);
630
+ // The bound applies to what the model sees (the text projection): display-only
631
+ // ui/image parts are bounded by their producers and must not push text over it.
632
+ const projected = textProjection(result);
633
+ if (JSON.stringify(projected).length > 48_000)
634
+ result = textResult(`${JSON.stringify(projected).slice(0, 40_000)}\n[tool output truncated]`, result.isError);
599
635
  if (pendingUpdate && effect === "read") {
600
636
  result = {
601
637
  ...result,
@@ -664,14 +700,15 @@ export class AgentRunner {
664
700
  // can simply prompt again to continue in the same session. The real hard stops remain the
665
701
  // token budget (`maxTokens`) and the timeout; the turn count is a safety rail.
666
702
  emit("run_turns_exceeded", { turns: maxTurns, maxTurns });
703
+ finish("turns_exceeded");
667
704
  return { sessionId, text: lastText, status: "turns-exceeded", usage: usageTotal };
668
705
  }
669
706
  catch (error) {
670
707
  // On cancellation report the abort reason, not the transport's secondary error.
671
708
  const cause = combined.aborted ? (combined.reason ?? error) : error;
672
- emit(combined.aborted ? "run_cancelled" : "run_failed", {
673
- error: cause instanceof Error ? cause.message : String(cause),
674
- });
709
+ const message = cause instanceof Error ? cause.message : String(cause);
710
+ emit(combined.aborted ? "run_cancelled" : "run_failed", { error: message });
711
+ finish(combined.aborted ? "cancelled" : "failed", message);
675
712
  throw error;
676
713
  }
677
714
  finally {
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /** @alisio/core public API: embed the Alisio agent programmatically. */
2
2
  export * from "@alisio/sdk";
3
+ export { type ActiveAgent, type AgentPickerItem, activeAgentCatalog, agentCatalogFromState, agentPickerItems, agentRunOptions, BUILTIN_AGENTS, DEFAULT_AGENT_ID, type MainCapableAgentRecord, mainAgentFromRecord, resolveActiveAgent, } from "./agents/active.ts";
3
4
  export { type AppOptions, type BuiltinContext, type BuiltinPlugin, createApplication, enabledBuiltins, type PluginCatalogEntry, type PluginCatalogStatus, } from "./application.ts";
4
- export { type Config, type ConfigLoadResult, type ConfigProvenance, configFile, configHome, configSchema, isSettableSettingKey, type LoadedConfig, loadConfig, loadConfigWithProvenance, type McpServerSource, overridesSavedProviderProfile, type SettableSettingKey, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.ts";
5
+ export { BUILTIN_COMMANDS, type BuiltinCommand, CommandCatalog, type CommandExecutionContext, type CommandHost, type CommandResult, type CommandSurface, } from "./commands/catalog.ts";
6
+ export { type Config, type ConfigLoadResult, type ConfigProvenance, configFile, configHome, configSchema, isSettableSettingKey, type LoadedConfig, loadConfig, loadConfigWithProvenance, type McpServerSource, overridesSavedProviderProfile, type SettableSettingInfo, type SettableSettingKey, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, settableSettings, stateHome, } from "./config.ts";
5
7
  export { type ContextBudget, checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.ts";
6
- export type { ApprovalDecision, ApprovalHandler, ApprovalRequest, ContextSource, HookFailure, Policy, RunnerExtensions, Session, SessionStore, } from "./core/contracts.ts";
8
+ export type { ApprovalDecision, ApprovalHandler, ApprovalRequest, BeginRunInput, ContextSource, EndRunInput, EventPage, HookFailure, MessagePage, PageOptions, Policy, RunnerExtensions, RunRecord, RunStatus, Session, SessionStore, StoredEvent, TerminalRunStatus, ToolCallMeta, } from "./core/contracts.ts";
7
9
  export { ToolRegistry } from "./core/registry.ts";
8
10
  export { AgentRunner, type CompactionResult, defaultTokenBudget, type RunnerOptions, type RunnerSettingsPatch, type RunOptions, } from "./core/runner.ts";
9
11
  export { type ExtensionConflict, ExtensionRegistry } from "./extensions/registry.ts";
@@ -13,14 +15,16 @@ export { discoverPlugins, PluginHost, type PluginHostOptions, pluginPrefix, } fr
13
15
  export { type CliInstallOptions, cliInstall, INSTALL_TIMEOUT_MS, type InstallPluginInput, type InstallPluginResult, type InstallRunner, installedNpmPlugins, installPlugin, npmInstallTarget, type PluginSpec, parsePluginSpec, pluginTrustRemark, printInstallResult, registerPluginInstallTool, sanitizeNpmError, } from "./plugins/install.ts";
14
16
  export { ActiveProvider, ProviderRegistry, UnconfiguredProvider } from "./providers/registry.ts";
15
17
  export { availableProviderModels, type ProviderModelCatalog, resolveProviderModel, } from "./providers/routing.ts";
16
- export { type ProviderProfile, type ProviderSettings, ProviderSettingsStore, } from "./providers/settings.ts";
18
+ export { maskSecret, type ProviderProfile, type ProviderSettings, ProviderSettingsStore, } from "./providers/settings.ts";
17
19
  export { type InstructionFile, ProjectContext, type ProjectContextOptions, } from "./resources/context.ts";
18
20
  export { expandSlashPrompt, loadPromptTemplates, type PromptDiagnostic, type PromptRequirement, type PromptSource, type PromptSourceKind, type PromptTemplate, parsePromptTemplate, promptSources, renderPromptTemplate, splitArguments, } from "./resources/prompts.ts";
19
21
  export { type Skill, type SkillCatalogEntry, type SkillRoot, type SkillScope, Skills, skillRoots, } from "./resources/skills.ts";
20
22
  export { type ExternalDirectoryDecision, type ExternalDirectoryHandler, type ExternalDirectoryRequest, PathAccess, type PathAccessOptions, type ResolvePathOptions, } from "./runtime/access.ts";
23
+ export { BlobStore, type BlobStoreOptions } from "./runtime/blobs.ts";
24
+ export { clipLines, unifiedPatch } from "./runtime/diff.ts";
21
25
  export { exists, fileSize, readHead, readJson, readText, which } from "./runtime/fs.ts";
22
26
  export { defaultGlobalRoots, isPathSpec, PLUGIN_KEYWORD, resolvePluginSpec, } from "./runtime/modules.ts";
23
- export { findWorkspace, outsideRootsMessage, safePath } from "./runtime/paths.ts";
27
+ export { findWorkspace, inside, outsideRootsMessage, safePath } from "./runtime/paths.ts";
24
28
  export { isMissingCommand, type ProcessResult, RIPGREP_INSTALL_HINT, runProcess, } from "./runtime/process.ts";
25
29
  export { isSqliteExperimentalWarning, openDatabase } from "./runtime/sqlite.ts";
26
30
  export { SQLiteStore } from "./runtime/store.ts";