@alisio/core 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/application.d.ts +192 -0
  4. package/dist/application.js +764 -0
  5. package/dist/config.d.ts +420 -0
  6. package/dist/config.js +416 -0
  7. package/dist/core/compaction.d.ts +48 -0
  8. package/dist/core/compaction.js +206 -0
  9. package/dist/core/contracts.d.ts +141 -0
  10. package/dist/core/contracts.js +1 -0
  11. package/dist/core/registry.d.ts +8 -0
  12. package/dist/core/registry.js +54 -0
  13. package/dist/core/runner.d.ts +121 -0
  14. package/dist/core/runner.js +543 -0
  15. package/dist/extensions/registry.d.ts +28 -0
  16. package/dist/extensions/registry.js +58 -0
  17. package/dist/index.d.ts +28 -0
  18. package/dist/index.js +27 -0
  19. package/dist/integrations/herdr.d.ts +34 -0
  20. package/dist/integrations/herdr.js +174 -0
  21. package/dist/mcp/connector.d.ts +63 -0
  22. package/dist/mcp/connector.js +384 -0
  23. package/dist/plugins/host.d.ts +164 -0
  24. package/dist/plugins/host.js +572 -0
  25. package/dist/providers/openai-compatible.d.ts +14 -0
  26. package/dist/providers/openai-compatible.js +228 -0
  27. package/dist/providers/registry.d.ts +35 -0
  28. package/dist/providers/registry.js +74 -0
  29. package/dist/providers/routing.d.ts +11 -0
  30. package/dist/providers/routing.js +41 -0
  31. package/dist/providers/settings.d.ts +30 -0
  32. package/dist/providers/settings.js +80 -0
  33. package/dist/resources/context.d.ts +39 -0
  34. package/dist/resources/context.js +164 -0
  35. package/dist/resources/prompts.d.ts +77 -0
  36. package/dist/resources/prompts.js +182 -0
  37. package/dist/resources/skills.d.ts +82 -0
  38. package/dist/resources/skills.js +214 -0
  39. package/dist/runtime/fs.d.ts +9 -0
  40. package/dist/runtime/fs.js +73 -0
  41. package/dist/runtime/modules.d.ts +14 -0
  42. package/dist/runtime/modules.js +76 -0
  43. package/dist/runtime/paths.d.ts +4 -0
  44. package/dist/runtime/paths.js +42 -0
  45. package/dist/runtime/process.d.ts +16 -0
  46. package/dist/runtime/process.js +104 -0
  47. package/dist/runtime/sqlite.d.ts +3 -0
  48. package/dist/runtime/sqlite.js +85 -0
  49. package/dist/runtime/store.d.ts +37 -0
  50. package/dist/runtime/store.js +250 -0
  51. package/dist/sessions/children.d.ts +55 -0
  52. package/dist/sessions/children.js +231 -0
  53. package/dist/startup/index.d.ts +41 -0
  54. package/dist/startup/index.js +105 -0
  55. package/dist/startup/mascot.d.ts +6 -0
  56. package/dist/startup/mascot.js +30 -0
  57. package/dist/startup/screen.d.ts +18 -0
  58. package/dist/startup/screen.js +116 -0
  59. package/dist/startup/text.d.ts +19 -0
  60. package/dist/startup/text.js +68 -0
  61. package/dist/tools/execute.d.ts +16 -0
  62. package/dist/tools/execute.js +96 -0
  63. package/dist/tools/search.d.ts +69 -0
  64. package/dist/tools/search.js +232 -0
  65. package/dist/tools/standard.d.ts +22 -0
  66. package/dist/tools/standard.js +408 -0
  67. package/dist/tools/webfetch.d.ts +26 -0
  68. package/dist/tools/webfetch.js +119 -0
  69. package/dist/trust.d.ts +34 -0
  70. package/dist/trust.js +113 -0
  71. package/package.json +62 -0
@@ -0,0 +1,37 @@
1
+ import { type Message, type SqlDatabase, type ToolCall, type ToolResult } from "@alisio/sdk";
2
+ import type { Session, SessionStore } from "../core/contracts.ts";
3
+ export declare class SQLiteStore implements SessionStore {
4
+ readonly db: SqlDatabase;
5
+ constructor(path: string);
6
+ /** Forward-only, idempotent migrations. Existing databases keep all rows. */
7
+ private migrate;
8
+ create(workspace: string, provider: string, model: string): Session;
9
+ private row;
10
+ get(id: string): Session;
11
+ /** Root sessions (children are listed through `children`). */
12
+ list(): Session[];
13
+ createChild(record: import("../core/contracts.ts").ChildSessionRecord): Session;
14
+ children(parentId: string): Session[];
15
+ updateSession(id: string, patch: {
16
+ status?: Session["status"];
17
+ usage?: {
18
+ input: number;
19
+ output: number;
20
+ };
21
+ }): void;
22
+ updateBinding(id: string, provider: string, model: string): void;
23
+ interruptStale(): number;
24
+ messages(id: string): Message[];
25
+ append(id: string, message: Message): void;
26
+ setModel(id: string, model: string): void;
27
+ compact(id: string, replaced: number, summary: Message): void;
28
+ acquire(id: string): void;
29
+ release(id: string): void;
30
+ beginCall(id: string, call: ToolCall): void;
31
+ endCall(id: string, call: ToolCall, result: ToolResult): void;
32
+ reconcile(id: string, acknowledge?: boolean): void;
33
+ event(id: string, runId: string, type: string, data: unknown): void;
34
+ getState(plugin: string, key: string): unknown;
35
+ setState(plugin: string, key: string, value: unknown): void;
36
+ close(): void;
37
+ }
@@ -0,0 +1,250 @@
1
+ import { textResult, } from "@alisio/sdk";
2
+ import { openDatabase } from "./sqlite.js";
3
+ export class SQLiteStore {
4
+ db;
5
+ constructor(path) {
6
+ this.db = openDatabase(path);
7
+ this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;
8
+ CREATE TABLE IF NOT EXISTS schema_migrations(version INTEGER PRIMARY KEY);
9
+ INSERT OR IGNORE INTO schema_migrations VALUES(1);
10
+ CREATE TABLE IF NOT EXISTS sessions(id TEXT PRIMARY KEY,workspace TEXT,provider TEXT,model TEXT,locked_pid INTEGER);
11
+ CREATE TABLE IF NOT EXISTS messages(seq INTEGER PRIMARY KEY AUTOINCREMENT,session TEXT REFERENCES sessions(id),body TEXT NOT NULL);
12
+ CREATE TABLE IF NOT EXISTS tool_calls(session TEXT,call_id TEXT,status TEXT,result TEXT,PRIMARY KEY(session,call_id));
13
+ CREATE TABLE IF NOT EXISTS events(seq INTEGER PRIMARY KEY AUTOINCREMENT,session TEXT,run_id TEXT,type TEXT,body TEXT);
14
+ CREATE TABLE IF NOT EXISTS plugin_state(plugin TEXT,key TEXT,value TEXT,PRIMARY KEY(plugin,key));`);
15
+ this.migrate();
16
+ }
17
+ /** Forward-only, idempotent migrations. Existing databases keep all rows. */
18
+ migrate() {
19
+ const has = (version) => !!this.db.prepare("SELECT 1 FROM schema_migrations WHERE version=?").get(version);
20
+ if (!has(2))
21
+ this.db.transaction(() => {
22
+ const columns = this.db.prepare("PRAGMA table_info(messages)").all();
23
+ // Compacted rows stay for audit but are excluded from the active conversation.
24
+ if (!columns.some((c) => c.name === "compacted"))
25
+ this.db.exec("ALTER TABLE messages ADD COLUMN compacted INTEGER NOT NULL DEFAULT 0");
26
+ this.db.exec("INSERT OR IGNORE INTO schema_migrations VALUES(2)");
27
+ });
28
+ if (!has(3))
29
+ this.db.transaction(() => {
30
+ // Child sessions: parent link, depth, agent label, lifecycle status, usage and spec.
31
+ const columns = new Set(this.db.prepare("PRAGMA table_info(sessions)").all().map((c) => c.name));
32
+ const add = (name, type) => {
33
+ if (!columns.has(name))
34
+ this.db.exec(`ALTER TABLE sessions ADD COLUMN ${name} ${type}`);
35
+ };
36
+ add("parent_id", "TEXT");
37
+ add("depth", "INTEGER NOT NULL DEFAULT 0");
38
+ add("agent", "TEXT");
39
+ add("status", "TEXT");
40
+ add("title", "TEXT");
41
+ add("usage", "TEXT");
42
+ add("options", "TEXT");
43
+ add("created_at", "INTEGER");
44
+ add("updated_at", "INTEGER");
45
+ this.db.exec("CREATE INDEX IF NOT EXISTS sessions_parent ON sessions(parent_id)");
46
+ this.db.exec("INSERT OR IGNORE INTO schema_migrations VALUES(3)");
47
+ });
48
+ }
49
+ create(workspace, provider, model) {
50
+ const id = crypto.randomUUID();
51
+ this.db
52
+ .prepare("INSERT INTO sessions(id,workspace,provider,model) VALUES(?,?,?,?)")
53
+ .run(id, workspace, provider, model);
54
+ return { id, workspace, provider, model };
55
+ }
56
+ row(r) {
57
+ return {
58
+ id: String(r.id),
59
+ workspace: String(r.workspace),
60
+ provider: String(r.provider),
61
+ model: String(r.model),
62
+ ...(r.parent_id ? { parentId: String(r.parent_id) } : {}),
63
+ ...(r.parent_id ? { depth: Number(r.depth ?? 0) } : {}),
64
+ ...(r.agent ? { agent: String(r.agent) } : {}),
65
+ ...(r.status ? { status: r.status } : {}),
66
+ ...(r.title ? { title: String(r.title) } : {}),
67
+ ...(r.usage ? { usage: JSON.parse(String(r.usage)) } : {}),
68
+ ...(r.options ? { options: JSON.parse(String(r.options)) } : {}),
69
+ ...(r.created_at ? { createdAt: Number(r.created_at) } : {}),
70
+ ...(r.updated_at ? { updatedAt: Number(r.updated_at) } : {}),
71
+ };
72
+ }
73
+ get(id) {
74
+ const row = this.db.prepare("SELECT * FROM sessions WHERE id=?").get(id);
75
+ if (!row)
76
+ throw new Error(`Session not found: ${id}`);
77
+ return this.row(row);
78
+ }
79
+ /** Root sessions (children are listed through `children`). */
80
+ list() {
81
+ return this.db
82
+ .prepare("SELECT * FROM sessions WHERE parent_id IS NULL ORDER BY rowid DESC")
83
+ .all()
84
+ .map((r) => this.row(r));
85
+ }
86
+ createChild(record) {
87
+ const id = record.id ?? crypto.randomUUID();
88
+ const now = Date.now();
89
+ this.db
90
+ .prepare(`INSERT INTO sessions(id,workspace,provider,model,parent_id,depth,agent,status,title,usage,options,created_at,updated_at)
91
+ VALUES(?,?,?,?,?,?,?,'queued',?,?,?,?,?)`)
92
+ .run(id, record.workspace, record.provider, record.model, record.parentId, record.depth, record.agent, record.title, JSON.stringify({ input: 0, output: 0 }), JSON.stringify(record.options), now, now);
93
+ return this.get(id);
94
+ }
95
+ children(parentId) {
96
+ return this.db
97
+ .prepare("SELECT * FROM sessions WHERE parent_id=? ORDER BY created_at, rowid")
98
+ .all(parentId)
99
+ .map((r) => this.row(r));
100
+ }
101
+ updateSession(id, patch) {
102
+ if (patch.status)
103
+ this.db
104
+ .prepare("UPDATE sessions SET status=?, updated_at=? WHERE id=?")
105
+ .run(patch.status, Date.now(), id);
106
+ if (patch.usage)
107
+ this.db
108
+ .prepare("UPDATE sessions SET usage=?, updated_at=? WHERE id=?")
109
+ .run(JSON.stringify(patch.usage), Date.now(), id);
110
+ }
111
+ updateBinding(id, provider, model) {
112
+ if (!provider.trim() || !model.trim())
113
+ throw new Error("Provider and model must not be empty");
114
+ this.get(id);
115
+ this.db
116
+ .prepare("UPDATE sessions SET provider=?, model=?, updated_at=? WHERE id=?")
117
+ .run(provider, model, Date.now(), id);
118
+ }
119
+ interruptStale() {
120
+ let count = 0;
121
+ const rows = this.db
122
+ .prepare("SELECT id, locked_pid FROM sessions WHERE status IN ('queued','running')")
123
+ .all();
124
+ for (const row of rows) {
125
+ if (row.locked_pid && row.locked_pid !== process.pid) {
126
+ try {
127
+ process.kill(row.locked_pid, 0);
128
+ continue; // Another live process owns it.
129
+ }
130
+ catch {
131
+ /* dead owner */
132
+ }
133
+ }
134
+ if (row.locked_pid === process.pid)
135
+ continue;
136
+ this.updateSession(row.id, { status: "interrupted" });
137
+ count++;
138
+ }
139
+ return count;
140
+ }
141
+ messages(id) {
142
+ return this.db
143
+ .prepare("SELECT body FROM messages WHERE session=? AND compacted=0 ORDER BY seq")
144
+ .all(id).map((x) => JSON.parse(x.body));
145
+ }
146
+ append(id, message) {
147
+ this.db
148
+ .prepare("INSERT INTO messages(session,body) VALUES(?,?)")
149
+ .run(id, JSON.stringify(message));
150
+ }
151
+ setModel(id, model) {
152
+ if (!model.trim())
153
+ throw new Error("Model ID must not be empty");
154
+ this.get(id);
155
+ this.db.prepare("UPDATE sessions SET model=? WHERE id=?").run(model, id);
156
+ }
157
+ compact(id, replaced, summary) {
158
+ this.db.transaction(() => {
159
+ const rows = this.db
160
+ .prepare("SELECT seq,body FROM messages WHERE session=? AND compacted=0 ORDER BY seq")
161
+ .all(id);
162
+ if (replaced < 1 || replaced > rows.length)
163
+ throw new Error("Compaction range does not match the active history");
164
+ // Keep ordering by seq: retire every active row, then write summary + kept copies.
165
+ this.db.prepare("UPDATE messages SET compacted=1 WHERE session=? AND compacted=0").run(id);
166
+ this.append(id, summary);
167
+ for (const row of rows.slice(replaced))
168
+ this.db.prepare("INSERT INTO messages(session,body) VALUES(?,?)").run(id, row.body);
169
+ });
170
+ }
171
+ acquire(id) {
172
+ this.db.transaction(() => {
173
+ this.get(id);
174
+ const row = this.db.prepare("SELECT locked_pid FROM sessions WHERE id=?").get(id);
175
+ if (row.locked_pid) {
176
+ let alive = true;
177
+ try {
178
+ process.kill(row.locked_pid, 0);
179
+ }
180
+ catch (e) {
181
+ if (e.code === "ESRCH")
182
+ alive = false;
183
+ }
184
+ if (alive)
185
+ throw new Error("Session is already in use");
186
+ }
187
+ this.db.prepare("UPDATE sessions SET locked_pid=? WHERE id=?").run(process.pid, id);
188
+ });
189
+ }
190
+ release(id) {
191
+ this.db
192
+ .prepare("UPDATE sessions SET locked_pid=NULL WHERE id=? AND locked_pid=?")
193
+ .run(id, process.pid);
194
+ }
195
+ beginCall(id, call) {
196
+ this.db
197
+ .prepare("INSERT INTO tool_calls(session,call_id,status) VALUES(?,?,'pending')")
198
+ .run(id, call.id);
199
+ }
200
+ endCall(id, call, result) {
201
+ this.db
202
+ .prepare("INSERT INTO tool_calls(session,call_id,status,result) VALUES(?,?,'completed',?) ON CONFLICT(session,call_id) DO UPDATE SET status='completed',result=excluded.result")
203
+ .run(id, call.id, JSON.stringify(result));
204
+ }
205
+ reconcile(id, acknowledge = false) {
206
+ this.db.transaction(() => {
207
+ const messages = this.messages(id);
208
+ const done = new Set(messages.filter((m) => m.role === "tool").map((m) => m.callId));
209
+ for (const m of messages) {
210
+ if (m.role !== "assistant")
211
+ continue;
212
+ for (const call of m.calls) {
213
+ if (done.has(call.id))
214
+ continue;
215
+ const row = this.db
216
+ .prepare("SELECT status,result FROM tool_calls WHERE session=? AND call_id=?")
217
+ .get(id, call.id);
218
+ if (row?.status === "pending" && !acknowledge)
219
+ throw new Error(`Uncertain tool outcome ${call.id}. Inspect effects, then use sessions recover ${id} --acknowledge`);
220
+ const result = row?.status === "completed" && row.result
221
+ ? JSON.parse(row.result)
222
+ : textResult(row
223
+ ? "Previous execution outcome is uncertain; user acknowledged it. Do not repeat automatically."
224
+ : "Previous run stopped before this tool was executed.", true);
225
+ this.endCall(id, call, result);
226
+ this.append(id, { role: "tool", callId: call.id, result });
227
+ }
228
+ }
229
+ });
230
+ }
231
+ event(id, runId, type, data) {
232
+ this.db
233
+ .prepare("INSERT INTO events(session,run_id,type,body) VALUES(?,?,?,?)")
234
+ .run(id, runId, type, JSON.stringify(data));
235
+ }
236
+ getState(plugin, key) {
237
+ const row = this.db
238
+ .prepare("SELECT value FROM plugin_state WHERE plugin=? AND key=?")
239
+ .get(plugin, key);
240
+ return row ? JSON.parse(row.value) : undefined;
241
+ }
242
+ setState(plugin, key, value) {
243
+ this.db
244
+ .prepare("INSERT INTO plugin_state VALUES(?,?,?) ON CONFLICT(plugin,key) DO UPDATE SET value=excluded.value")
245
+ .run(plugin, key, JSON.stringify(value));
246
+ }
247
+ close() {
248
+ this.db.close();
249
+ }
250
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Generic child sessions (the `api.sessions` extension point). A child is a separate,
3
+ * persisted conversation with a parent link that runs through the same runner with narrowed
4
+ * permissions: it can never gain a capability, tool or approval its parent lacks. Nothing here
5
+ * knows about "agents" or delegation policies; plugins build those on top.
6
+ */
7
+ import type { ChildRunResult, ChildSessionInfo, ChildSessionSpec, PluginAPI, ResolvedProviderModel, SessionStatus } from "@alisio/sdk";
8
+ import type { ContextSource, Policy, SessionStore } from "../core/contracts.ts";
9
+ import type { AgentRunner } from "../core/runner.ts";
10
+ type SessionsAPI = PluginAPI["sessions"];
11
+ export interface ChildSessionsOptions {
12
+ store: SessionStore;
13
+ runner: AgentRunner;
14
+ providerId: () => string;
15
+ resolveModel?: (reference: string, signal?: AbortSignal) => Promise<ResolvedProviderModel>;
16
+ bindProvider?: (sessionId: string, target: ResolvedProviderModel) => Promise<string>;
17
+ /** The root policy (the runner's; it may widen after "allow for session" approvals). */
18
+ rootPolicy: () => Policy;
19
+ rootApprovals: boolean;
20
+ readOnly: boolean;
21
+ contextFor: (workspace: string) => ContextSource;
22
+ }
23
+ export declare class ChildSessions implements SessionsAPI {
24
+ private o;
25
+ private cancelled;
26
+ constructor(o: ChildSessionsOptions);
27
+ private spec;
28
+ private info;
29
+ capabilities(id: string): {
30
+ write: boolean;
31
+ process: boolean;
32
+ approvals: boolean;
33
+ readOnly: boolean;
34
+ };
35
+ /** Tool filter for a session: the parent's filter AND the child's own allow/deny/effects. */
36
+ private filter;
37
+ spawn(spec: ChildSessionSpec): ChildSessionInfo;
38
+ create(spec: ChildSessionSpec): Promise<ChildSessionInfo>;
39
+ /** Agent path from the top-level child down, e.g. "general › explore". */
40
+ private label;
41
+ run(id: string, prompt: string, options?: {
42
+ signal?: AbortSignal;
43
+ }): Promise<ChildRunResult>;
44
+ get(id: string): ChildSessionInfo | undefined;
45
+ children(parentId: string): ChildSessionInfo[];
46
+ ancestors(id: string): string[];
47
+ private descendants;
48
+ cancel(id: string): number;
49
+ enqueue(id: string, text: string): void;
50
+ isRunning(id: string): boolean;
51
+ model(id: string): string;
52
+ workspace(id: string): string;
53
+ setStatus(id: string, status: SessionStatus): void;
54
+ }
55
+ export {};
@@ -0,0 +1,231 @@
1
+ const STATE_CHANGING = new Set(["write", "process", "external"]);
2
+ export class ChildSessions {
3
+ o;
4
+ cancelled = new Set();
5
+ constructor(o) {
6
+ this.o = o;
7
+ // Work left running by a previous process is never restarted automatically.
8
+ o.store.interruptStale();
9
+ }
10
+ spec(session) {
11
+ return (session.options ?? {});
12
+ }
13
+ info(session) {
14
+ if (!session.parentId)
15
+ throw new Error(`Not a child session: ${session.id}`);
16
+ return {
17
+ id: session.id,
18
+ parentId: session.parentId,
19
+ depth: session.depth ?? 1,
20
+ agent: session.agent ?? "agent",
21
+ title: session.title ?? "",
22
+ status: session.status ?? "queued",
23
+ model: session.model,
24
+ provider: session.provider,
25
+ workspace: session.workspace,
26
+ usage: session.usage ?? { input: 0, output: 0 },
27
+ capabilities: (({ readOnly: _r, ...caps }) => caps)(this.capabilities(session.id)),
28
+ createdAt: session.createdAt ?? 0,
29
+ updatedAt: session.updatedAt ?? 0,
30
+ };
31
+ }
32
+ capabilities(id) {
33
+ const session = this.o.store.get(id);
34
+ if (!session.parentId) {
35
+ const policy = this.o.rootPolicy();
36
+ return {
37
+ write: policy.write,
38
+ process: policy.process,
39
+ approvals: this.o.rootApprovals && !this.o.readOnly,
40
+ readOnly: this.o.readOnly,
41
+ };
42
+ }
43
+ const parent = this.capabilities(session.parentId);
44
+ const spec = this.spec(session);
45
+ const readOnly = parent.readOnly || !!spec.readOnly;
46
+ const level = (effect) => spec.permission?.[effect] ?? "allow";
47
+ return {
48
+ write: parent.write && !readOnly && level("write") === "allow",
49
+ process: parent.process && !readOnly && level("process") === "allow",
50
+ approvals: parent.approvals && !readOnly && (level("write") !== "deny" || level("process") !== "deny"),
51
+ readOnly,
52
+ };
53
+ }
54
+ /** Tool filter for a session: the parent's filter AND the child's own allow/deny/effects. */
55
+ filter(id) {
56
+ const session = this.o.store.get(id);
57
+ if (!session.parentId)
58
+ return () => true;
59
+ const parent = this.filter(session.parentId);
60
+ const spec = this.spec(session);
61
+ const caps = this.capabilities(id);
62
+ const allow = spec.tools?.allow;
63
+ const deny = new Set(spec.tools?.deny ?? []);
64
+ return (tool) => {
65
+ if (!parent(tool) || deny.has(tool.name))
66
+ return false;
67
+ if (allow && !allow.includes("*") && !allow.includes(tool.name))
68
+ return false;
69
+ const effect = tool.effect ?? "external";
70
+ if (caps.readOnly && STATE_CHANGING.has(effect))
71
+ return false;
72
+ for (const e of ["write", "process"])
73
+ if (effect === e && spec.permission?.[e] === "deny")
74
+ return false;
75
+ return true;
76
+ };
77
+ }
78
+ spawn(spec) {
79
+ const parent = this.o.store.get(spec.parentId);
80
+ if (spec.id && !/^[0-9a-f-]{36}$/i.test(spec.id))
81
+ throw new Error("Child id must be a UUID");
82
+ const stored = {
83
+ ...(spec.instructions ? { instructions: spec.instructions } : {}),
84
+ ...(spec.tools ? { tools: spec.tools } : {}),
85
+ ...(spec.readOnly ? { readOnly: true } : {}),
86
+ ...(spec.permission ? { permission: spec.permission } : {}),
87
+ ...(spec.maxTurns ? { maxTurns: spec.maxTurns } : {}),
88
+ ...(spec.timeoutMs ? { timeoutMs: spec.timeoutMs } : {}),
89
+ ...(spec.maxTokens ? { maxTokens: spec.maxTokens } : {}),
90
+ };
91
+ const child = this.o.store.createChild({
92
+ ...(spec.id ? { id: spec.id } : {}),
93
+ parentId: parent.id,
94
+ workspace: spec.workspace ?? parent.workspace,
95
+ provider: parent.provider,
96
+ model: spec.model ?? parent.model,
97
+ agent: spec.agent,
98
+ title: spec.title,
99
+ depth: (parent.depth ?? 0) + 1,
100
+ options: stored,
101
+ });
102
+ return this.info(child);
103
+ }
104
+ async create(spec) {
105
+ if (!spec.model)
106
+ return this.spawn(spec);
107
+ if (!this.o.resolveModel || !this.o.bindProvider)
108
+ throw new Error("Cross-provider model resolution is not available");
109
+ const target = await this.o.resolveModel(spec.model);
110
+ const child = this.spawn({ ...spec, model: target.model.id });
111
+ try {
112
+ const provider = await this.o.bindProvider(child.id, target);
113
+ this.o.store.updateBinding(child.id, provider, target.model.id);
114
+ return this.info(this.o.store.get(child.id));
115
+ }
116
+ catch (error) {
117
+ this.o.store.updateSession(child.id, { status: "failed" });
118
+ throw error;
119
+ }
120
+ }
121
+ /** Agent path from the top-level child down, e.g. "general › explore". */
122
+ label(id) {
123
+ return [...this.ancestors(id).reverse(), id]
124
+ .map((x) => this.o.store.get(x))
125
+ .filter((s) => s.parentId)
126
+ .map((s) => s.agent ?? "agent")
127
+ .join(" › ");
128
+ }
129
+ async run(id, prompt, options = {}) {
130
+ const session = this.o.store.get(id);
131
+ if (!session.parentId)
132
+ throw new Error(`Not a child session: ${id}`);
133
+ if (this.o.runner.isRunning(id))
134
+ throw new Error(`Session ${id} is already running`);
135
+ const spec = this.spec(session);
136
+ const caps = this.capabilities(id);
137
+ const root = this.o.rootPolicy();
138
+ const signals = [options.signal, this.o.runner.signal(session.parentId)].filter((s) => !!s);
139
+ this.cancelled.delete(id);
140
+ this.o.store.updateSession(id, { status: "running" });
141
+ const usage = { ...(session.usage ?? { input: 0, output: 0 }) };
142
+ try {
143
+ const result = await this.o.runner.run(id, prompt, signals.length ? AbortSignal.any(signals) : undefined, {
144
+ ...(spec.instructions ? { instructions: spec.instructions } : {}),
145
+ toolFilter: this.filter(id),
146
+ policy: {
147
+ write: caps.write,
148
+ process: caps.process,
149
+ external: root.external && !caps.readOnly,
150
+ },
151
+ approvals: caps.approvals,
152
+ label: this.label(id),
153
+ workspace: session.workspace,
154
+ context: this.o.contextFor(session.workspace),
155
+ ...(spec.maxTurns ? { maxTurns: spec.maxTurns } : {}),
156
+ ...(spec.maxTokens ? { maxTokens: spec.maxTokens } : {}),
157
+ ...(spec.timeoutMs ? { timeoutMs: spec.timeoutMs } : {}),
158
+ });
159
+ usage.input += result.usage.input;
160
+ usage.output += result.usage.output;
161
+ this.o.store.updateSession(id, { status: "completed", usage });
162
+ return { id, status: "completed", text: result.text, usage };
163
+ }
164
+ catch (error) {
165
+ const aborted = this.cancelled.has(id) || signals.some((s) => s.aborted);
166
+ const status = aborted ? "cancelled" : "failed";
167
+ this.o.store.updateSession(id, { status });
168
+ return {
169
+ id,
170
+ status,
171
+ text: "",
172
+ usage,
173
+ error: error instanceof Error ? error.message : String(error),
174
+ };
175
+ }
176
+ }
177
+ get(id) {
178
+ try {
179
+ const session = this.o.store.get(id);
180
+ return session.parentId ? this.info(session) : undefined;
181
+ }
182
+ catch {
183
+ return undefined;
184
+ }
185
+ }
186
+ children(parentId) {
187
+ return this.o.store.children(parentId).map((s) => this.info(s));
188
+ }
189
+ ancestors(id) {
190
+ const out = [];
191
+ let current = this.o.store.get(id);
192
+ while (current.parentId) {
193
+ out.push(current.parentId);
194
+ current = this.o.store.get(current.parentId);
195
+ }
196
+ return out;
197
+ }
198
+ descendants(id) {
199
+ return this.o.store.children(id).flatMap((c) => [c.id, ...this.descendants(c.id)]);
200
+ }
201
+ cancel(id) {
202
+ let running = 0;
203
+ for (const target of [id, ...this.descendants(id)]) {
204
+ const session = this.o.store.get(target);
205
+ if (this.o.runner.isRunning(target)) {
206
+ this.cancelled.add(target);
207
+ this.o.runner.abort(target, new Error("Cancelled by the user"));
208
+ running++;
209
+ }
210
+ else if (session.parentId && session.status === "queued")
211
+ this.o.store.updateSession(target, { status: "cancelled" });
212
+ }
213
+ return running;
214
+ }
215
+ enqueue(id, text) {
216
+ this.o.runner.enqueue(id, text);
217
+ }
218
+ isRunning(id) {
219
+ return this.o.runner.isRunning(id);
220
+ }
221
+ model(id) {
222
+ return this.o.store.get(id).model;
223
+ }
224
+ workspace(id) {
225
+ return this.o.store.get(id).workspace;
226
+ }
227
+ setStatus(id, status) {
228
+ if (this.o.store.get(id).parentId)
229
+ this.o.store.updateSession(id, { status });
230
+ }
231
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Startup pipeline: resolve the startup-screen and mascot providers from the extension
3
+ * registry, validate their output, and fall back to the defaults (with a diagnostic) when a
4
+ * provider throws, returns garbage or is too slow. Never throws.
5
+ */
6
+ import type { PluginMetadata, StartupFact, TerminalCapabilities } from "@alisio/sdk";
7
+ import type { ExtensionConflict, ExtensionRegistry } from "../extensions/registry.ts";
8
+ export { DefaultAlisioMascot } from "./mascot.ts";
9
+ export { composeSideBySide, DefaultStartupScreen, infoSection, mascotSection, pluginsSection, shortenPath, startupTips, tipsSection, titleSection, welcomeSection, } from "./screen.ts";
10
+ export { padEnd, sanitizeLine, stripAnsi, truncate, visibleWidth } from "./text.ts";
11
+ export type StartupDiagnostic = ({
12
+ type: "extension_conflict";
13
+ } & ExtensionConflict) | {
14
+ type: "plugin_hook_failed";
15
+ source: string;
16
+ hook: string;
17
+ error: string;
18
+ };
19
+ export interface StartupInput {
20
+ version: string;
21
+ cwd: string;
22
+ /** Home directory used to shorten the working directory as `~/…`. */
23
+ home?: string;
24
+ model?: string;
25
+ provider?: string;
26
+ userName?: string;
27
+ terminal?: TerminalCapabilities;
28
+ facts?: StartupFact[];
29
+ tipSeed?: number;
30
+ }
31
+ export interface StartupResult {
32
+ lines: string[];
33
+ screen: string;
34
+ mascot: string;
35
+ diagnostics: StartupDiagnostic[];
36
+ }
37
+ interface Source {
38
+ extensions: ExtensionRegistry;
39
+ metadata(): PluginMetadata[];
40
+ }
41
+ export declare function renderStartup(source: Source, input: StartupInput): StartupResult;