@alisio/core 0.1.0-alpha.18 → 0.1.0-alpha.20
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/application.d.ts +3 -0
- package/dist/application.js +3 -0
- package/dist/commands/builtins.js +8 -3
- package/dist/commands/catalog.d.ts +3 -0
- package/dist/commands/catalog.js +25 -1
- package/dist/core/runner.d.ts +17 -0
- package/dist/core/runner.js +24 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/connector.js +1 -1
- package/dist/runtime/store.d.ts +8 -2
- package/dist/runtime/store.js +20 -3
- package/dist/sessions/side-questions.d.ts +57 -0
- package/dist/sessions/side-questions.js +161 -0
- package/package.json +2 -2
package/dist/application.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { Skills } from "./resources/skills.ts";
|
|
|
13
13
|
import { type ExternalDirectoryHandler, PathAccess } from "./runtime/access.ts";
|
|
14
14
|
import { BlobStore } from "./runtime/blobs.ts";
|
|
15
15
|
import { SQLiteStore } from "./runtime/store.ts";
|
|
16
|
+
import { SideQuestions } from "./sessions/side-questions.ts";
|
|
16
17
|
export interface AppOptions {
|
|
17
18
|
cwd?: string;
|
|
18
19
|
config?: string;
|
|
@@ -209,6 +210,8 @@ export declare function createApplication(options?: AppOptions): Promise<{
|
|
|
209
210
|
profileName: undefined;
|
|
210
211
|
} | undefined;
|
|
211
212
|
runner: AgentRunner;
|
|
213
|
+
/** `/btw` side questions: tool-less answers about a session outside its conversation. */
|
|
214
|
+
sideQuestions: SideQuestions;
|
|
212
215
|
herdr: HerdrBridge;
|
|
213
216
|
contextWindow: (model: string) => number | undefined;
|
|
214
217
|
/** Effective context budget + compaction point shared by the runner and the TUI bar. */
|
package/dist/application.js
CHANGED
|
@@ -20,6 +20,7 @@ import { isPathSpec } from "./runtime/modules.js";
|
|
|
20
20
|
import { findWorkspace } from "./runtime/paths.js";
|
|
21
21
|
import { SQLiteStore } from "./runtime/store.js";
|
|
22
22
|
import { ChildSessions } from "./sessions/children.js";
|
|
23
|
+
import { SideQuestions } from "./sessions/side-questions.js";
|
|
23
24
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
25
|
/**
|
|
25
26
|
* Cap per teardown stage of `app.close()`: exit must never wait longer than this on a hanging
|
|
@@ -859,6 +860,8 @@ export async function createApplication(options = {}) {
|
|
|
859
860
|
return providerInfo;
|
|
860
861
|
},
|
|
861
862
|
runner,
|
|
863
|
+
/** `/btw` side questions: tool-less answers about a session outside its conversation. */
|
|
864
|
+
sideQuestions: new SideQuestions({ runner, state: store }),
|
|
862
865
|
herdr,
|
|
863
866
|
contextWindow,
|
|
864
867
|
/** Effective context budget + compaction point shared by the runner and the TUI bar. */
|
|
@@ -59,18 +59,16 @@ export const BUILTIN_COMMANDS = [
|
|
|
59
59
|
{
|
|
60
60
|
name: "plugins",
|
|
61
61
|
description: "Browse and manage project plugins",
|
|
62
|
-
aliases: ["plugin"],
|
|
63
62
|
surfaces: ALL,
|
|
64
63
|
execution: "core",
|
|
65
64
|
},
|
|
66
65
|
{
|
|
67
66
|
name: "skills",
|
|
68
67
|
description: "Browse and manage effective skills",
|
|
69
|
-
aliases: ["skill"],
|
|
70
68
|
surfaces: ALL,
|
|
71
69
|
execution: "core",
|
|
72
70
|
},
|
|
73
|
-
{ name: "
|
|
71
|
+
{ name: "mcps", description: "Browse and manage MCP servers", surfaces: ALL, execution: "core" },
|
|
74
72
|
{
|
|
75
73
|
name: "settings",
|
|
76
74
|
description: "Open the settings menu",
|
|
@@ -105,6 +103,13 @@ export const BUILTIN_COMMANDS = [
|
|
|
105
103
|
surfaces: INTERACTIVE,
|
|
106
104
|
execution: "surface",
|
|
107
105
|
},
|
|
106
|
+
{
|
|
107
|
+
name: "btw",
|
|
108
|
+
description: "Ask a side question about the session without adding to the conversation",
|
|
109
|
+
argumentHint: "[question]",
|
|
110
|
+
surfaces: ALL,
|
|
111
|
+
execution: "core",
|
|
112
|
+
},
|
|
108
113
|
{
|
|
109
114
|
name: "exit",
|
|
110
115
|
description: "Exit Alisio",
|
|
@@ -11,6 +11,7 @@ import type { AgentRunner } from "../core/runner.ts";
|
|
|
11
11
|
import type { McpServerInfo } from "../mcp/connector.ts";
|
|
12
12
|
import type { PromptTemplate } from "../resources/prompts.ts";
|
|
13
13
|
import type { SkillCatalogEntry } from "../resources/skills.ts";
|
|
14
|
+
import { type SideQuestions } from "../sessions/side-questions.ts";
|
|
14
15
|
import { BUILTIN_COMMANDS, type BuiltinCommand } from "./builtins.ts";
|
|
15
16
|
export { BUILTIN_COMMANDS, type BuiltinCommand };
|
|
16
17
|
export type CommandSurface = CommandDescriptor["surfaces"][number];
|
|
@@ -55,6 +56,8 @@ export interface CommandHost {
|
|
|
55
56
|
};
|
|
56
57
|
};
|
|
57
58
|
updateSetting?(key: "agents.effort", value: unknown): Promise<string>;
|
|
59
|
+
/** `/btw` side questions (tool-less, outside the conversation). */
|
|
60
|
+
sideQuestions?: Pick<SideQuestions, "ask" | "history">;
|
|
58
61
|
}
|
|
59
62
|
export interface CommandExecutionContext {
|
|
60
63
|
sessionId: string;
|
package/dist/commands/catalog.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { agentCatalogFromState, resolveActiveAgent } from "../agents/active.js";
|
|
2
|
+
import { formatSideQuestion, SIDE_QUESTION_DESCRIPTION, SIDE_QUESTION_USAGE, } from "../sessions/side-questions.js";
|
|
2
3
|
import { BUILTIN_COMMANDS } from "./builtins.js";
|
|
3
4
|
export { BUILTIN_COMMANDS };
|
|
4
5
|
const builtinDescriptor = (c) => ({
|
|
@@ -148,7 +149,7 @@ const HANDLERS = {
|
|
|
148
149
|
data: plugins,
|
|
149
150
|
};
|
|
150
151
|
},
|
|
151
|
-
async
|
|
152
|
+
async mcps(_args, _ctx, host) {
|
|
152
153
|
const servers = host.mcp?.list() ?? [];
|
|
153
154
|
if (!servers.length)
|
|
154
155
|
return { text: "No MCP servers configured", tone: "notice" };
|
|
@@ -159,6 +160,29 @@ const HANDLERS = {
|
|
|
159
160
|
data: servers,
|
|
160
161
|
};
|
|
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
|
+
},
|
|
162
186
|
async agents(args, ctx, host) {
|
|
163
187
|
// With arguments, `/agents <verb>` belongs to the subagents plugin's own command (as in the
|
|
164
188
|
// TUI); without them it lists the active-agent catalog.
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -157,6 +157,23 @@ export declare class AgentRunner {
|
|
|
157
157
|
basis: "window" | "unknown";
|
|
158
158
|
compactionAt: number;
|
|
159
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
|
+
}>;
|
|
160
177
|
/** Change the model used by subsequent turns of a session; recorded in the store. */
|
|
161
178
|
setModel(sessionId: string, model: string): void;
|
|
162
179
|
/** Manually compact a session outside a run. */
|
package/dist/core/runner.js
CHANGED
|
@@ -155,6 +155,30 @@ export class AgentRunner {
|
|
|
155
155
|
};
|
|
156
156
|
return { total: undefined, basis: "unknown", compactionAt: 100 };
|
|
157
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* What a tool-less side call about a session needs (for example a `/btw` side question): the
|
|
160
|
+
* session's provider and model, its system instructions, its ACTIVE history and the current
|
|
161
|
+
* limits. Claims no lock and writes nothing, so it is safe while a run is active.
|
|
162
|
+
*/
|
|
163
|
+
async sideContext(sessionId) {
|
|
164
|
+
const o = this.options;
|
|
165
|
+
const session = o.store.get(sessionId);
|
|
166
|
+
const provider = await (o.providerFor?.(session) ?? o.provider);
|
|
167
|
+
const window = o.contextWindow?.(session.model);
|
|
168
|
+
return {
|
|
169
|
+
provider,
|
|
170
|
+
model: session.model,
|
|
171
|
+
...(window !== undefined && window > 0 && window <= MAX_TRUSTED_WINDOW
|
|
172
|
+
? { contextWindow: window }
|
|
173
|
+
: {}),
|
|
174
|
+
instructions: await o.context.instructions(sessionId),
|
|
175
|
+
messages: o.store.messages(sessionId),
|
|
176
|
+
maxContextChars: o.maxContextChars ?? 800_000,
|
|
177
|
+
maxOutputTokens: o.maxOutputTokens ?? 4096,
|
|
178
|
+
timeoutMs: o.timeoutMs ?? 300_000,
|
|
179
|
+
...(o.reasoningEffort ? { reasoningEffort: o.reasoningEffort } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
158
182
|
/** Change the model used by subsequent turns of a session; recorded in the store. */
|
|
159
183
|
setModel(sessionId, model) {
|
|
160
184
|
const id = model.trim();
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export { isMissingCommand, type ProcessResult, RIPGREP_INSTALL_HINT, runProcess,
|
|
|
29
29
|
export { isSqliteExperimentalWarning, openDatabase } from "./runtime/sqlite.ts";
|
|
30
30
|
export { SQLiteStore } from "./runtime/store.ts";
|
|
31
31
|
export { ChildSessions, type ChildSessionsOptions } from "./sessions/children.ts";
|
|
32
|
+
export { formatSideQuestion, SIDE_QUESTION_DESCRIPTION, SIDE_QUESTION_HISTORY_LIMIT, SIDE_QUESTION_INSTRUCTIONS, SIDE_QUESTION_MAX_CHARS, SIDE_QUESTION_USAGE, SIDE_QUESTIONS_NAMESPACE, type SideQuestionEntry, SideQuestions, type SideQuestionsOptions, sideTranscript, } from "./sessions/side-questions.ts";
|
|
32
33
|
export { composeSideBySide, DefaultAlisioMascot, DefaultStartupScreen, infoSection, mascotSection, padEnd, pluginsSection, renderStartup, type StartupDiagnostic, type StartupInput, type StartupResult, sanitizeLine, shortenPath, startupTips, stripAnsi, tipsSection, titleSection, truncate, visibleWidth, welcomeSection, } from "./startup/index.ts";
|
|
33
34
|
export { hash, objectSchema, registerStandard } from "./tools/standard.ts";
|
|
34
35
|
export { getTrust, hashProjectConfig, hasProjectResources, listTrust, resolveTrust, revokeTrust, setTrust, type TrustEntry, type TrustListing, type TrustResolution, trustStorePath, } from "./trust.ts";
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export { isMissingCommand, RIPGREP_INSTALL_HINT, runProcess, } from "./runtime/p
|
|
|
28
28
|
export { isSqliteExperimentalWarning, openDatabase } from "./runtime/sqlite.js";
|
|
29
29
|
export { SQLiteStore } from "./runtime/store.js";
|
|
30
30
|
export { ChildSessions } from "./sessions/children.js";
|
|
31
|
+
export { formatSideQuestion, SIDE_QUESTION_DESCRIPTION, SIDE_QUESTION_HISTORY_LIMIT, SIDE_QUESTION_INSTRUCTIONS, SIDE_QUESTION_MAX_CHARS, SIDE_QUESTION_USAGE, SIDE_QUESTIONS_NAMESPACE, SideQuestions, sideTranscript, } from "./sessions/side-questions.js";
|
|
31
32
|
export { composeSideBySide, DefaultAlisioMascot, DefaultStartupScreen, infoSection, mascotSection, padEnd, pluginsSection, renderStartup, sanitizeLine, shortenPath, startupTips, stripAnsi, tipsSection, titleSection, truncate, visibleWidth, welcomeSection, } from "./startup/index.js";
|
|
32
33
|
export { hash, objectSchema, registerStandard } from "./tools/standard.js";
|
|
33
34
|
export { getTrust, hashProjectConfig, hasProjectResources, listTrust, resolveTrust, revokeTrust, setTrust, trustStorePath, } from "./trust.js";
|
package/dist/mcp/connector.js
CHANGED
|
@@ -219,7 +219,7 @@ export class McpConnector {
|
|
|
219
219
|
if (!config)
|
|
220
220
|
throw new Error(`Unknown MCP server: ${name}`);
|
|
221
221
|
if (!config.enabled)
|
|
222
|
-
throw new Error(`MCP server "${name}" is disabled; enable it in /
|
|
222
|
+
throw new Error(`MCP server "${name}" is disabled; enable it in /mcps`);
|
|
223
223
|
if (this.clients.has(name))
|
|
224
224
|
return this.registeredNames(name);
|
|
225
225
|
if (config.transport === "http" &&
|
package/dist/runtime/store.d.ts
CHANGED
|
@@ -71,20 +71,26 @@ export declare class SQLiteStore implements SessionStore {
|
|
|
71
71
|
eventsPage(session: string, options?: PageOptions): EventPage;
|
|
72
72
|
/**
|
|
73
73
|
* Known workspaces for web clients: the workspaces of root sessions plus the ones recorded with
|
|
74
|
-
* `recordWorkspace` (v4 `workspaces` table), with their UI metadata
|
|
74
|
+
* `recordWorkspace` (v4 `workspaces` table), with their UI metadata (v5: `archivedAt`) and
|
|
75
|
+
* root-session count.
|
|
75
76
|
*/
|
|
76
77
|
workspaces(): Array<{
|
|
77
78
|
path: string;
|
|
78
79
|
label?: string;
|
|
79
80
|
pinned: boolean;
|
|
80
81
|
lastOpenedAt?: number;
|
|
82
|
+
archivedAt?: number;
|
|
81
83
|
sessions: number;
|
|
82
84
|
}>;
|
|
83
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Records (upserts) a workspace and patches its UI metadata (`label: null` clears the label;
|
|
87
|
+
* `archived` stamps or clears `archived_at` and never touches its sessions).
|
|
88
|
+
*/
|
|
84
89
|
recordWorkspace(path: string, patch?: {
|
|
85
90
|
label?: string | null;
|
|
86
91
|
pinned?: boolean;
|
|
87
92
|
lastOpenedAt?: number;
|
|
93
|
+
archived?: boolean;
|
|
88
94
|
}): void;
|
|
89
95
|
/**
|
|
90
96
|
* Patches web-facing session metadata: title, pin, archive flag and `options` (shallow-merged
|
package/dist/runtime/store.js
CHANGED
|
@@ -104,6 +104,14 @@ export class SQLiteStore {
|
|
|
104
104
|
CREATE INDEX IF NOT EXISTS messages_session ON messages(session, seq);
|
|
105
105
|
INSERT OR IGNORE INTO schema_migrations VALUES(4);`);
|
|
106
106
|
});
|
|
107
|
+
if (!has(5))
|
|
108
|
+
this.db.transaction(() => {
|
|
109
|
+
// v5 (additive): archived workspaces (hidden from the default web list).
|
|
110
|
+
const columns = this.db.prepare("PRAGMA table_info(workspaces)").all();
|
|
111
|
+
if (!columns.some((c) => c.name === "archived_at"))
|
|
112
|
+
this.db.exec("ALTER TABLE workspaces ADD COLUMN archived_at INTEGER");
|
|
113
|
+
this.db.exec("INSERT OR IGNORE INTO schema_migrations VALUES(5)");
|
|
114
|
+
});
|
|
107
115
|
}
|
|
108
116
|
create(workspace, provider, model) {
|
|
109
117
|
const id = crypto.randomUUID();
|
|
@@ -461,11 +469,12 @@ export class SQLiteStore {
|
|
|
461
469
|
}
|
|
462
470
|
/**
|
|
463
471
|
* Known workspaces for web clients: the workspaces of root sessions plus the ones recorded with
|
|
464
|
-
* `recordWorkspace` (v4 `workspaces` table), with their UI metadata
|
|
472
|
+
* `recordWorkspace` (v4 `workspaces` table), with their UI metadata (v5: `archivedAt`) and
|
|
473
|
+
* root-session count.
|
|
465
474
|
*/
|
|
466
475
|
workspaces() {
|
|
467
476
|
const rows = this.db
|
|
468
|
-
.prepare(`SELECT p.path, w.label, COALESCE(w.pinned, 0) AS pinned, w.last_opened_at,
|
|
477
|
+
.prepare(`SELECT p.path, w.label, COALESCE(w.pinned, 0) AS pinned, w.last_opened_at, w.archived_at,
|
|
469
478
|
(SELECT count(*) FROM sessions s WHERE s.workspace=p.path AND s.parent_id IS NULL) AS sessions
|
|
470
479
|
FROM (SELECT workspace AS path FROM sessions WHERE parent_id IS NULL AND workspace IS NOT NULL
|
|
471
480
|
UNION SELECT path FROM workspaces) p
|
|
@@ -477,10 +486,14 @@ export class SQLiteStore {
|
|
|
477
486
|
...(r.label != null ? { label: String(r.label) } : {}),
|
|
478
487
|
pinned: !!r.pinned,
|
|
479
488
|
...(r.last_opened_at != null ? { lastOpenedAt: Number(r.last_opened_at) } : {}),
|
|
489
|
+
...(r.archived_at != null ? { archivedAt: Number(r.archived_at) } : {}),
|
|
480
490
|
sessions: Number(r.sessions ?? 0),
|
|
481
491
|
}));
|
|
482
492
|
}
|
|
483
|
-
/**
|
|
493
|
+
/**
|
|
494
|
+
* Records (upserts) a workspace and patches its UI metadata (`label: null` clears the label;
|
|
495
|
+
* `archived` stamps or clears `archived_at` and never touches its sessions).
|
|
496
|
+
*/
|
|
484
497
|
recordWorkspace(path, patch = {}) {
|
|
485
498
|
this.db.transaction(() => {
|
|
486
499
|
this.db.prepare("INSERT OR IGNORE INTO workspaces(path) VALUES(?)").run(path);
|
|
@@ -494,6 +507,10 @@ export class SQLiteStore {
|
|
|
494
507
|
this.db
|
|
495
508
|
.prepare("UPDATE workspaces SET last_opened_at=? WHERE path=?")
|
|
496
509
|
.run(patch.lastOpenedAt, path);
|
|
510
|
+
if (patch.archived !== undefined)
|
|
511
|
+
this.db
|
|
512
|
+
.prepare("UPDATE workspaces SET archived_at=? WHERE path=?")
|
|
513
|
+
.run(patch.archived ? Date.now() : null, path);
|
|
497
514
|
});
|
|
498
515
|
}
|
|
499
516
|
/**
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/btw` side questions: a question about a session answered by one separate, tool-less model
|
|
3
|
+
* call that sees the session's active history, without becoming part of the conversation.
|
|
4
|
+
*
|
|
5
|
+
* Guarantees: nothing is written to `messages`, `tool_calls`, `runs`, the durable `events` or the
|
|
6
|
+
* session usage, and the session lock is never claimed, so a side question may run while a run
|
|
7
|
+
* is active (it has its own abort signal). The only thing persisted is this service's own
|
|
8
|
+
* history, in `plugin_state` under the reserved namespace `core:btw` (plugin ids cannot contain
|
|
9
|
+
* `:`, so no plugin can read or overwrite it), keyed by session id and capped to the newest
|
|
10
|
+
* `SIDE_QUESTION_HISTORY_LIMIT` entries.
|
|
11
|
+
*/
|
|
12
|
+
import type { Message, SideQuestionEntry } from "@alisio/sdk";
|
|
13
|
+
import type { AgentRunner } from "../core/runner.ts";
|
|
14
|
+
export type { SideQuestionEntry };
|
|
15
|
+
/** `plugin_state.plugin` value of the side-question history (never a valid plugin id). */
|
|
16
|
+
export declare const SIDE_QUESTIONS_NAMESPACE = "core:btw";
|
|
17
|
+
export declare const SIDE_QUESTION_HISTORY_LIMIT = 20;
|
|
18
|
+
export declare const SIDE_QUESTION_MAX_CHARS = 4000;
|
|
19
|
+
/** The line every surface prints for `/btw` without a question and without any history. */
|
|
20
|
+
export declare const SIDE_QUESTION_USAGE = "Usage: /btw <question>";
|
|
21
|
+
export declare const SIDE_QUESTION_DESCRIPTION = "Ask a side question about the current session without adding to the conversation.";
|
|
22
|
+
export declare const SIDE_QUESTION_INSTRUCTIONS: string;
|
|
23
|
+
interface StateStore {
|
|
24
|
+
getState(plugin: string, key: string): unknown;
|
|
25
|
+
setState(plugin: string, key: string, value: unknown): void;
|
|
26
|
+
}
|
|
27
|
+
export interface SideQuestionsOptions {
|
|
28
|
+
runner: Pick<AgentRunner, "sideContext">;
|
|
29
|
+
/** Where the history lives (the SQLite store's `plugin_state`). */
|
|
30
|
+
state: StateStore;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
id?: () => string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The transcript lines for the side call, newest kept first: when the whole history does not fit
|
|
36
|
+
* `budget` characters, the oldest messages are dropped (and counted in a leading marker). The
|
|
37
|
+
* newest message alone is clipped from its start if it is still too large.
|
|
38
|
+
*/
|
|
39
|
+
export declare function sideTranscript(messages: Message[], budget: number): string;
|
|
40
|
+
export declare class SideQuestions {
|
|
41
|
+
private readonly options;
|
|
42
|
+
private readonly now;
|
|
43
|
+
private readonly newId;
|
|
44
|
+
constructor(options: SideQuestionsOptions);
|
|
45
|
+
/** The session's side questions, oldest first (newest last). */
|
|
46
|
+
history(sessionId: string): SideQuestionEntry[];
|
|
47
|
+
/**
|
|
48
|
+
* Answers `question` about the session with one tool-less call of the session's model and
|
|
49
|
+
* records it in the history. Throws on an empty or oversized question, on cancellation (the
|
|
50
|
+
* signal's reason) and on provider failures (nothing is recorded then).
|
|
51
|
+
*/
|
|
52
|
+
ask(sessionId: string, question: string, options?: {
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
}): Promise<SideQuestionEntry>;
|
|
55
|
+
}
|
|
56
|
+
/** Plain Markdown for one entry (`position` is 1-based): readline, API callers and `/btw`. */
|
|
57
|
+
export declare function formatSideQuestion(entry: SideQuestionEntry, position: number, total: number): string;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { serializeForSummary } from "../core/compaction.js";
|
|
2
|
+
/** `plugin_state.plugin` value of the side-question history (never a valid plugin id). */
|
|
3
|
+
export const SIDE_QUESTIONS_NAMESPACE = "core:btw";
|
|
4
|
+
export const SIDE_QUESTION_HISTORY_LIMIT = 20;
|
|
5
|
+
export const SIDE_QUESTION_MAX_CHARS = 4_000;
|
|
6
|
+
/** The line every surface prints for `/btw` without a question and without any history. */
|
|
7
|
+
export const SIDE_QUESTION_USAGE = "Usage: /btw <question>";
|
|
8
|
+
export const SIDE_QUESTION_DESCRIPTION = "Ask a side question about the current session without adding to the conversation.";
|
|
9
|
+
/** Per-message clip of the transcript the side call sees (as in compaction summaries). */
|
|
10
|
+
const MAX_ITEM_CHARS = 4_000;
|
|
11
|
+
export const SIDE_QUESTION_INSTRUCTIONS = [
|
|
12
|
+
"The user is asking a quick side question (/btw) about the session transcript below.",
|
|
13
|
+
"It is NOT part of the conversation: answer it directly and concisely from the transcript and",
|
|
14
|
+
"your general knowledge. You have no tools in this reply, so do not take or promise any",
|
|
15
|
+
"action, do not continue the task and do not call tools; if the transcript does not contain",
|
|
16
|
+
"the answer, say so briefly.",
|
|
17
|
+
].join(" ");
|
|
18
|
+
/**
|
|
19
|
+
* The transcript lines for the side call, newest kept first: when the whole history does not fit
|
|
20
|
+
* `budget` characters, the oldest messages are dropped (and counted in a leading marker). The
|
|
21
|
+
* newest message alone is clipped from its start if it is still too large.
|
|
22
|
+
*/
|
|
23
|
+
export function sideTranscript(messages, budget) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
let used = 0;
|
|
26
|
+
let omitted = 0;
|
|
27
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
28
|
+
const message = messages[i];
|
|
29
|
+
const text = serializeForSummary([message], MAX_ITEM_CHARS);
|
|
30
|
+
if (used + text.length + 1 > budget) {
|
|
31
|
+
if (lines.length)
|
|
32
|
+
omitted = i + 1;
|
|
33
|
+
else {
|
|
34
|
+
// Even the newest message alone is too large: keep its end (the most recent part).
|
|
35
|
+
lines.push(text.slice(Math.max(0, text.length - budget)));
|
|
36
|
+
omitted = i;
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
lines.unshift(text);
|
|
41
|
+
used += text.length + 1;
|
|
42
|
+
}
|
|
43
|
+
if (omitted > 0)
|
|
44
|
+
lines.unshift(`[${omitted} earlier messages omitted to fit the context budget]`);
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
47
|
+
const isEntry = (value) => !!value &&
|
|
48
|
+
typeof value === "object" &&
|
|
49
|
+
typeof value.id === "string" &&
|
|
50
|
+
typeof value.question === "string" &&
|
|
51
|
+
typeof value.answer === "string";
|
|
52
|
+
export class SideQuestions {
|
|
53
|
+
options;
|
|
54
|
+
now;
|
|
55
|
+
newId;
|
|
56
|
+
constructor(options) {
|
|
57
|
+
this.options = options;
|
|
58
|
+
this.now = options.now ?? Date.now;
|
|
59
|
+
this.newId = options.id ?? (() => crypto.randomUUID());
|
|
60
|
+
}
|
|
61
|
+
/** The session's side questions, oldest first (newest last). */
|
|
62
|
+
history(sessionId) {
|
|
63
|
+
let stored;
|
|
64
|
+
try {
|
|
65
|
+
stored = this.options.state.getState(SIDE_QUESTIONS_NAMESPACE, sessionId);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
return Array.isArray(stored) ? stored.filter(isEntry) : [];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Answers `question` about the session with one tool-less call of the session's model and
|
|
74
|
+
* records it in the history. Throws on an empty or oversized question, on cancellation (the
|
|
75
|
+
* signal's reason) and on provider failures (nothing is recorded then).
|
|
76
|
+
*/
|
|
77
|
+
async ask(sessionId, question, options = {}) {
|
|
78
|
+
const text = question.trim();
|
|
79
|
+
if (!text)
|
|
80
|
+
throw new Error(SIDE_QUESTION_USAGE);
|
|
81
|
+
if (text.length > SIDE_QUESTION_MAX_CHARS)
|
|
82
|
+
throw new Error(`Side questions are limited to ${SIDE_QUESTION_MAX_CHARS} characters`);
|
|
83
|
+
const side = await this.options.runner.sideContext(sessionId);
|
|
84
|
+
const signal = AbortSignal.any([
|
|
85
|
+
AbortSignal.timeout(side.timeoutMs),
|
|
86
|
+
...(options.signal ? [options.signal] : []),
|
|
87
|
+
]);
|
|
88
|
+
signal.throwIfAborted();
|
|
89
|
+
const instructions = `${side.instructions}\n\n${SIDE_QUESTION_INSTRUCTIONS}`;
|
|
90
|
+
// The request must fit both the char budget and (when known) the model window, leaving room
|
|
91
|
+
// for the answer; ~3 chars per token keeps a conservative margin over the usual ~4.
|
|
92
|
+
const windowChars = side.contextWindow
|
|
93
|
+
? Math.max(0, side.contextWindow - side.maxOutputTokens) * 3
|
|
94
|
+
: Number.POSITIVE_INFINITY;
|
|
95
|
+
const budget = Math.max(2_000, Math.min(side.maxContextChars, windowChars) - instructions.length - text.length - 200);
|
|
96
|
+
const transcript = sideTranscript(side.messages, budget);
|
|
97
|
+
const prompt = transcript
|
|
98
|
+
? `<transcript>\n${transcript}\n</transcript>\n\nSide question: ${text}`
|
|
99
|
+
: `(The session has no messages yet.)\n\nSide question: ${text}`;
|
|
100
|
+
let answer = "";
|
|
101
|
+
let completed = false;
|
|
102
|
+
let truncated = false;
|
|
103
|
+
let usage = { input: 0, output: 0 };
|
|
104
|
+
try {
|
|
105
|
+
for await (const event of side.provider.stream({
|
|
106
|
+
instructions,
|
|
107
|
+
messages: [{ role: "user", text: prompt }],
|
|
108
|
+
tools: [],
|
|
109
|
+
maxOutputTokens: side.maxOutputTokens,
|
|
110
|
+
signal,
|
|
111
|
+
model: side.model,
|
|
112
|
+
sessionId,
|
|
113
|
+
...(side.reasoningEffort ? { reasoningEffort: side.reasoningEffort } : {}),
|
|
114
|
+
})) {
|
|
115
|
+
signal.throwIfAborted();
|
|
116
|
+
if (event.type === "text_delta")
|
|
117
|
+
answer += event.delta;
|
|
118
|
+
else if (event.type === "completed") {
|
|
119
|
+
completed = true;
|
|
120
|
+
truncated = event.message.truncated === true;
|
|
121
|
+
answer = event.message.text || answer;
|
|
122
|
+
if (event.usage)
|
|
123
|
+
usage = { input: event.usage.input, output: event.usage.output };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (signal.aborted)
|
|
129
|
+
throw signal.reason ?? error;
|
|
130
|
+
throw new Error(`Side question failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
131
|
+
}
|
|
132
|
+
signal.throwIfAborted();
|
|
133
|
+
if (!completed)
|
|
134
|
+
throw new Error("Side question failed: the provider did not complete");
|
|
135
|
+
answer = answer.trim();
|
|
136
|
+
if (!answer)
|
|
137
|
+
throw new Error("Side question failed: the model returned an empty answer");
|
|
138
|
+
const entry = {
|
|
139
|
+
id: this.newId(),
|
|
140
|
+
question: text,
|
|
141
|
+
answer,
|
|
142
|
+
model: side.model,
|
|
143
|
+
usage,
|
|
144
|
+
createdAt: this.now(),
|
|
145
|
+
...(truncated ? { truncated: true } : {}),
|
|
146
|
+
};
|
|
147
|
+
const next = [...this.history(sessionId), entry].slice(-SIDE_QUESTION_HISTORY_LIMIT);
|
|
148
|
+
this.options.state.setState(SIDE_QUESTIONS_NAMESPACE, sessionId, next);
|
|
149
|
+
return entry;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Plain Markdown for one entry (`position` is 1-based): readline, API callers and `/btw`. */
|
|
153
|
+
export function formatSideQuestion(entry, position, total) {
|
|
154
|
+
return [
|
|
155
|
+
`**btw ${position}/${total}** · ${entry.question.replace(/\s+/g, " ")}`,
|
|
156
|
+
"",
|
|
157
|
+
entry.answer,
|
|
158
|
+
"",
|
|
159
|
+
`_${entry.model} · ${entry.usage.input} in · ${entry.usage.output} out tokens${entry.truncated ? " · truncated" : ""}_`,
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20",
|
|
4
4
|
"description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=22.16"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@alisio/sdk": "0.1.0-alpha.
|
|
44
|
+
"@alisio/sdk": "0.1.0-alpha.16",
|
|
45
45
|
"@mixmark-io/domino": "2.2.0",
|
|
46
46
|
"@modelcontextprotocol/client": "2.1.0",
|
|
47
47
|
"ajv": "8.20.0",
|