@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.
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/application.d.ts +192 -0
- package/dist/application.js +764 -0
- package/dist/config.d.ts +420 -0
- package/dist/config.js +416 -0
- package/dist/core/compaction.d.ts +48 -0
- package/dist/core/compaction.js +206 -0
- package/dist/core/contracts.d.ts +141 -0
- package/dist/core/contracts.js +1 -0
- package/dist/core/registry.d.ts +8 -0
- package/dist/core/registry.js +54 -0
- package/dist/core/runner.d.ts +121 -0
- package/dist/core/runner.js +543 -0
- package/dist/extensions/registry.d.ts +28 -0
- package/dist/extensions/registry.js +58 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +27 -0
- package/dist/integrations/herdr.d.ts +34 -0
- package/dist/integrations/herdr.js +174 -0
- package/dist/mcp/connector.d.ts +63 -0
- package/dist/mcp/connector.js +384 -0
- package/dist/plugins/host.d.ts +164 -0
- package/dist/plugins/host.js +572 -0
- package/dist/providers/openai-compatible.d.ts +14 -0
- package/dist/providers/openai-compatible.js +228 -0
- package/dist/providers/registry.d.ts +35 -0
- package/dist/providers/registry.js +74 -0
- package/dist/providers/routing.d.ts +11 -0
- package/dist/providers/routing.js +41 -0
- package/dist/providers/settings.d.ts +30 -0
- package/dist/providers/settings.js +80 -0
- package/dist/resources/context.d.ts +39 -0
- package/dist/resources/context.js +164 -0
- package/dist/resources/prompts.d.ts +77 -0
- package/dist/resources/prompts.js +182 -0
- package/dist/resources/skills.d.ts +82 -0
- package/dist/resources/skills.js +214 -0
- package/dist/runtime/fs.d.ts +9 -0
- package/dist/runtime/fs.js +73 -0
- package/dist/runtime/modules.d.ts +14 -0
- package/dist/runtime/modules.js +76 -0
- package/dist/runtime/paths.d.ts +4 -0
- package/dist/runtime/paths.js +42 -0
- package/dist/runtime/process.d.ts +16 -0
- package/dist/runtime/process.js +104 -0
- package/dist/runtime/sqlite.d.ts +3 -0
- package/dist/runtime/sqlite.js +85 -0
- package/dist/runtime/store.d.ts +37 -0
- package/dist/runtime/store.js +250 -0
- package/dist/sessions/children.d.ts +55 -0
- package/dist/sessions/children.js +231 -0
- package/dist/startup/index.d.ts +41 -0
- package/dist/startup/index.js +105 -0
- package/dist/startup/mascot.d.ts +6 -0
- package/dist/startup/mascot.js +30 -0
- package/dist/startup/screen.d.ts +18 -0
- package/dist/startup/screen.js +116 -0
- package/dist/startup/text.d.ts +19 -0
- package/dist/startup/text.js +68 -0
- package/dist/tools/execute.d.ts +16 -0
- package/dist/tools/execute.js +96 -0
- package/dist/tools/search.d.ts +69 -0
- package/dist/tools/search.js +232 -0
- package/dist/tools/standard.d.ts +22 -0
- package/dist/tools/standard.js +408 -0
- package/dist/tools/webfetch.d.ts +26 -0
- package/dist/tools/webfetch.js +119 -0
- package/dist/trust.d.ts +34 -0
- package/dist/trust.js +113 -0
- package/package.json +62 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { textResult, } from "@alisio/sdk";
|
|
2
|
+
import { checkpointInstructions, estimateTokens, parseCheckpointOutput, planCompaction, shouldCompact, summarize, summaryMessage, } from "./compaction.js";
|
|
3
|
+
/**
|
|
4
|
+
* Default cumulative token budget for one run: several context windows (each turn re-sends the
|
|
5
|
+
* context), clamped; 1M tokens when the window is unknown.
|
|
6
|
+
*/
|
|
7
|
+
export function defaultTokenBudget(contextWindow) {
|
|
8
|
+
if (!contextWindow || contextWindow <= 0)
|
|
9
|
+
return 1_000_000;
|
|
10
|
+
return Math.min(8_000_000, Math.max(400_000, contextWindow * 8));
|
|
11
|
+
}
|
|
12
|
+
/** Extension output appended to history is capped regardless of what hooks return. */
|
|
13
|
+
const MAX_INJECT_CHARS = 24_000;
|
|
14
|
+
const allowed = (policy, effect) => effect === "read" || effect === "internal" || !!policy[effect];
|
|
15
|
+
export class AgentRunner {
|
|
16
|
+
options;
|
|
17
|
+
/** Sessions with an active run or compaction (one at a time per session). */
|
|
18
|
+
active = new Map();
|
|
19
|
+
/** Messages queued for a session's next turn (e.g. input for a running child). */
|
|
20
|
+
inbox = new Map();
|
|
21
|
+
/** Last provider-reported context size per session and the history length it covered. */
|
|
22
|
+
reported = new Map();
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.options = options;
|
|
25
|
+
}
|
|
26
|
+
/** Current capability policy (may widen when the user approves an effect for the session). */
|
|
27
|
+
get policy() {
|
|
28
|
+
return this.options.policy;
|
|
29
|
+
}
|
|
30
|
+
/** Whether write/process tools can be approved interactively. */
|
|
31
|
+
get approvals() {
|
|
32
|
+
return !!this.options.approve;
|
|
33
|
+
}
|
|
34
|
+
emitter(sessionId) {
|
|
35
|
+
const runId = crypto.randomUUID();
|
|
36
|
+
let seq = 0;
|
|
37
|
+
return (type, data) => {
|
|
38
|
+
const event = {
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
runId,
|
|
41
|
+
sessionId,
|
|
42
|
+
seq: ++seq,
|
|
43
|
+
type,
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
data,
|
|
46
|
+
};
|
|
47
|
+
if (type !== "text_delta" && type !== "tool_progress" && type !== "reasoning_delta")
|
|
48
|
+
this.options.store.event(sessionId, runId, type, data);
|
|
49
|
+
try {
|
|
50
|
+
this.options.onEvent?.(event);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* An observer cannot invalidate an executed operation. */
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Tools offered to the model under the current policy (and optional run narrowing). */
|
|
58
|
+
availableTools(run = {}) {
|
|
59
|
+
const o = this.options;
|
|
60
|
+
const policy = run.policy ?? o.policy;
|
|
61
|
+
const approvals = !!o.approve && run.approvals !== false;
|
|
62
|
+
return o.registry.list().filter((t) => {
|
|
63
|
+
if (run.toolFilter && !run.toolFilter(t))
|
|
64
|
+
return false;
|
|
65
|
+
const effect = t.effect ?? "external";
|
|
66
|
+
if (allowed(policy, effect))
|
|
67
|
+
return true;
|
|
68
|
+
return approvals && (effect === "write" || effect === "process" || effect === "external");
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Abort signal of the session's active run, if any (used to cascade cancellation). */
|
|
72
|
+
signal(sessionId) {
|
|
73
|
+
return this.active.get(sessionId)?.signal;
|
|
74
|
+
}
|
|
75
|
+
isRunning(sessionId) {
|
|
76
|
+
return this.active.has(sessionId);
|
|
77
|
+
}
|
|
78
|
+
/** Aborts the active run (or compaction) of a session. */
|
|
79
|
+
abort(sessionId, reason = new Error("Cancelled")) {
|
|
80
|
+
const controller = this.active.get(sessionId);
|
|
81
|
+
if (!controller)
|
|
82
|
+
return false;
|
|
83
|
+
controller.abort(reason);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/** Queues text as a user message for the session's next turn (or next run when idle). */
|
|
87
|
+
enqueue(sessionId, text) {
|
|
88
|
+
const queue = this.inbox.get(sessionId) ?? [];
|
|
89
|
+
queue.push(text.slice(0, 50_000));
|
|
90
|
+
this.inbox.set(sessionId, queue.slice(-20));
|
|
91
|
+
}
|
|
92
|
+
drain(sessionId) {
|
|
93
|
+
const queue = this.inbox.get(sessionId) ?? [];
|
|
94
|
+
this.inbox.delete(sessionId);
|
|
95
|
+
return queue;
|
|
96
|
+
}
|
|
97
|
+
claim(sessionId) {
|
|
98
|
+
if (this.active.has(sessionId))
|
|
99
|
+
throw new Error("Session is busy; await the current run or cancel it");
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
this.active.set(sessionId, controller);
|
|
102
|
+
return controller;
|
|
103
|
+
}
|
|
104
|
+
toolsText(tools) {
|
|
105
|
+
return JSON.stringify(tools.map((t) => ({ name: t.name, schema: t.inputSchema, description: t.description })));
|
|
106
|
+
}
|
|
107
|
+
/** Estimated tokens the next request of a session would send. */
|
|
108
|
+
async estimateContext(sessionId) {
|
|
109
|
+
const instructions = await this.options.context.instructions(sessionId);
|
|
110
|
+
return (estimateTokens(instructions) +
|
|
111
|
+
estimateTokens(this.options.store.messages(sessionId)) +
|
|
112
|
+
estimateTokens(this.toolsText(this.availableTools())));
|
|
113
|
+
}
|
|
114
|
+
/** Change the model used by subsequent turns of a session; recorded in the store. */
|
|
115
|
+
setModel(sessionId, model) {
|
|
116
|
+
const id = model.trim();
|
|
117
|
+
if (!id)
|
|
118
|
+
throw new Error("Model ID must not be empty");
|
|
119
|
+
const previous = this.options.store.get(sessionId).model;
|
|
120
|
+
if (previous === id)
|
|
121
|
+
return;
|
|
122
|
+
this.options.store.setModel(sessionId, id);
|
|
123
|
+
this.reported.delete(sessionId);
|
|
124
|
+
this.emitter(sessionId)("model_changed", { model: id, previous });
|
|
125
|
+
}
|
|
126
|
+
/** Manually compact a session outside a run. */
|
|
127
|
+
async compact(sessionId, request = {}) {
|
|
128
|
+
const controller = this.claim(sessionId);
|
|
129
|
+
const o = this.options;
|
|
130
|
+
let acquired = false;
|
|
131
|
+
const timeout = AbortSignal.timeout(o.timeoutMs ?? 300_000);
|
|
132
|
+
const signal = AbortSignal.any([
|
|
133
|
+
controller.signal,
|
|
134
|
+
timeout,
|
|
135
|
+
...(request.signal ? [request.signal] : []),
|
|
136
|
+
]);
|
|
137
|
+
try {
|
|
138
|
+
o.store.acquire(sessionId);
|
|
139
|
+
acquired = true;
|
|
140
|
+
o.store.reconcile(sessionId);
|
|
141
|
+
const session = o.store.get(sessionId);
|
|
142
|
+
return await this.compactLocked(sessionId, this.emitter(sessionId), session.model, signal, "manual", request.focus);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
if (acquired)
|
|
146
|
+
o.store.release(sessionId);
|
|
147
|
+
this.active.delete(sessionId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async compactLocked(sessionId, emit, model, signal, reason, focus) {
|
|
151
|
+
const o = this.options;
|
|
152
|
+
const messages = o.store.messages(sessionId);
|
|
153
|
+
const plan = planCompaction(messages, { keepTurns: o.compaction?.keepTurns ?? 2 });
|
|
154
|
+
const before = await this.estimateContext(sessionId);
|
|
155
|
+
if (!plan) {
|
|
156
|
+
emit("compaction_skipped", { reason, before, detail: "Not enough history to compact" });
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
emit("compaction_started", { reason, before, messages: plan.summarized.length });
|
|
160
|
+
const reportFailures = (failures) => {
|
|
161
|
+
for (const f of failures)
|
|
162
|
+
emit("plugin_hook_failed", { ...f, continued: true });
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
const ext = o.extensions;
|
|
166
|
+
const hookInput = { sessionId, reason, messages: plan.summarized, focus };
|
|
167
|
+
let contribution = {
|
|
168
|
+
instructions: [],
|
|
169
|
+
fields: {},
|
|
170
|
+
failures: [],
|
|
171
|
+
};
|
|
172
|
+
if (ext)
|
|
173
|
+
try {
|
|
174
|
+
contribution = await ext.beforeCompact(hookInput);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
contribution.failures.push({
|
|
178
|
+
source: "host",
|
|
179
|
+
hook: "beforeCompact",
|
|
180
|
+
error: String(error),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
reportFailures(contribution.failures);
|
|
184
|
+
const fields = Object.fromEntries(Object.entries(contribution.fields).map(([name, f]) => [name, f.description]));
|
|
185
|
+
const provider = await (o.providerFor?.(o.store.get(sessionId)) ?? o.provider);
|
|
186
|
+
const raw = await summarize(provider, {
|
|
187
|
+
instructions: checkpointInstructions(contribution.instructions, fields),
|
|
188
|
+
messages: plan.summarized,
|
|
189
|
+
focus,
|
|
190
|
+
model,
|
|
191
|
+
maxOutputTokens: o.maxOutputTokens ?? 4096,
|
|
192
|
+
signal,
|
|
193
|
+
sessionId,
|
|
194
|
+
});
|
|
195
|
+
const output = parseCheckpointOutput(raw, Object.keys(fields));
|
|
196
|
+
let after = {
|
|
197
|
+
inject: [],
|
|
198
|
+
reports: {},
|
|
199
|
+
failures: [],
|
|
200
|
+
};
|
|
201
|
+
if (ext)
|
|
202
|
+
try {
|
|
203
|
+
after = await ext.afterCompact({
|
|
204
|
+
...hookInput,
|
|
205
|
+
model,
|
|
206
|
+
replaced: plan.cut,
|
|
207
|
+
structured: output.structured,
|
|
208
|
+
...(output.checkpoint ? { checkpoint: output.checkpoint } : {}),
|
|
209
|
+
checkpointText: output.text,
|
|
210
|
+
extracted: output.extracted,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
after.failures.push({ source: "host", hook: "afterCompact", error: String(error) });
|
|
215
|
+
}
|
|
216
|
+
reportFailures(after.failures);
|
|
217
|
+
const body = [output.text, ...after.inject.map((x) => x.slice(0, MAX_INJECT_CHARS))]
|
|
218
|
+
.filter((x) => x.trim())
|
|
219
|
+
.join("\n\n");
|
|
220
|
+
const summary = summaryMessage(body, plan.cut);
|
|
221
|
+
o.store.compact(sessionId, plan.cut, summary);
|
|
222
|
+
this.reported.delete(sessionId);
|
|
223
|
+
const afterTokens = await this.estimateContext(sessionId);
|
|
224
|
+
emit("compaction_completed", {
|
|
225
|
+
reason,
|
|
226
|
+
before,
|
|
227
|
+
after: afterTokens,
|
|
228
|
+
replaced: plan.cut,
|
|
229
|
+
structured: output.structured,
|
|
230
|
+
summarizedTokens: estimateTokens(plan.summarized),
|
|
231
|
+
checkpointTokens: estimateTokens(summary.text),
|
|
232
|
+
plugins: after.reports,
|
|
233
|
+
});
|
|
234
|
+
return { replaced: plan.cut, before, after: afterTokens, summary };
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
emit("compaction_failed", {
|
|
238
|
+
reason,
|
|
239
|
+
error: error instanceof Error ? error.message : String(error),
|
|
240
|
+
});
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async run(sessionId, prompt, signal, options = {}) {
|
|
245
|
+
const controller = this.claim(sessionId);
|
|
246
|
+
const o = this.options, emit = this.emitter(sessionId);
|
|
247
|
+
let tokens = 0, lastText = "", acquired = false;
|
|
248
|
+
const usageTotal = { input: 0, output: 0 };
|
|
249
|
+
const timeout = AbortSignal.timeout(options.timeoutMs ?? o.timeoutMs ?? 300_000);
|
|
250
|
+
const combined = AbortSignal.any([controller.signal, timeout, ...(signal ? [signal] : [])]);
|
|
251
|
+
const context = options.context ?? o.context;
|
|
252
|
+
const workspace = options.workspace ?? o.workspace;
|
|
253
|
+
const policy = options.policy ?? o.policy;
|
|
254
|
+
const approvals = !!o.approve && options.approvals !== false;
|
|
255
|
+
const withPersona = async () => {
|
|
256
|
+
const base = await context.instructions(sessionId);
|
|
257
|
+
return options.instructions ? `${base}\n\n${options.instructions}` : base;
|
|
258
|
+
};
|
|
259
|
+
try {
|
|
260
|
+
o.store.acquire(sessionId);
|
|
261
|
+
acquired = true;
|
|
262
|
+
o.store.reconcile(sessionId);
|
|
263
|
+
const session = o.store.get(sessionId);
|
|
264
|
+
const provider = await (o.providerFor?.(session) ?? o.provider);
|
|
265
|
+
if (session.provider !== provider.id || session.workspace !== workspace)
|
|
266
|
+
throw new Error("Session provider/workspace mismatch");
|
|
267
|
+
// The session records the model; switching models is explicit via setModel.
|
|
268
|
+
const model = session.model;
|
|
269
|
+
combined.throwIfAborted();
|
|
270
|
+
if (o.extensions && o.store.messages(sessionId).length === 0) {
|
|
271
|
+
// New session: extensions may inject budgeted context once, persisted with the session.
|
|
272
|
+
try {
|
|
273
|
+
const start = await o.extensions.sessionStart({
|
|
274
|
+
sessionId,
|
|
275
|
+
model,
|
|
276
|
+
workspace,
|
|
277
|
+
});
|
|
278
|
+
for (const f of start.failures)
|
|
279
|
+
emit("plugin_hook_failed", { ...f, continued: true });
|
|
280
|
+
const text = start.inject
|
|
281
|
+
.map((x) => x.text.slice(0, MAX_INJECT_CHARS))
|
|
282
|
+
.filter((x) => x.trim())
|
|
283
|
+
.join("\n\n");
|
|
284
|
+
if (text) {
|
|
285
|
+
o.store.append(sessionId, { role: "user", text, summary: true });
|
|
286
|
+
emit("session_context_injected", {
|
|
287
|
+
tokens: estimateTokens(text),
|
|
288
|
+
sources: start.inject.map((x) => x.source),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
emit("plugin_hook_failed", {
|
|
294
|
+
source: "host",
|
|
295
|
+
hook: "sessionStart",
|
|
296
|
+
error: String(error),
|
|
297
|
+
continued: true,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
combined.throwIfAborted();
|
|
301
|
+
}
|
|
302
|
+
for (const queued of this.drain(sessionId))
|
|
303
|
+
o.store.append(sessionId, { role: "user", text: queued });
|
|
304
|
+
o.store.append(sessionId, {
|
|
305
|
+
role: "user",
|
|
306
|
+
text: prompt,
|
|
307
|
+
...(options.display ? { display: options.display } : {}),
|
|
308
|
+
...(options.attachments?.length ? { attachments: options.attachments } : {}),
|
|
309
|
+
});
|
|
310
|
+
emit("run_started", { model });
|
|
311
|
+
const budget = options.maxTokens ?? o.maxTokens ?? defaultTokenBudget(o.contextWindow?.(model));
|
|
312
|
+
const compaction = o.compaction ?? {};
|
|
313
|
+
for (let turn = 0; turn < (options.maxTurns ?? o.maxTurns ?? 20); turn++) {
|
|
314
|
+
combined.throwIfAborted();
|
|
315
|
+
// Input queued while the run was working (e.g. a parent's message to a child).
|
|
316
|
+
if (turn > 0)
|
|
317
|
+
for (const queued of this.drain(sessionId))
|
|
318
|
+
o.store.append(sessionId, { role: "user", text: queued });
|
|
319
|
+
let instructions = await withPersona();
|
|
320
|
+
let messages = o.store.messages(sessionId);
|
|
321
|
+
const tools = this.availableTools(options);
|
|
322
|
+
const toolsText = this.toolsText(tools);
|
|
323
|
+
const chars = () => instructions.length + JSON.stringify(messages).length + toolsText.length;
|
|
324
|
+
const used = () => {
|
|
325
|
+
const last = this.reported.get(sessionId);
|
|
326
|
+
return last && last.count <= messages.length
|
|
327
|
+
? last.tokens + estimateTokens(messages.slice(last.count))
|
|
328
|
+
: estimateTokens(instructions) + estimateTokens(messages) + estimateTokens(toolsText);
|
|
329
|
+
};
|
|
330
|
+
const overChars = chars() > (o.maxContextChars ?? 160_000);
|
|
331
|
+
const overWindow = compaction.auto !== false &&
|
|
332
|
+
shouldCompact(used(), o.contextWindow?.(model), compaction.threshold ?? 0.85);
|
|
333
|
+
if (overWindow || (overChars && compaction.auto !== false)) {
|
|
334
|
+
await this.compactLocked(sessionId, emit, model, combined, "auto");
|
|
335
|
+
instructions = await withPersona();
|
|
336
|
+
messages = o.store.messages(sessionId);
|
|
337
|
+
}
|
|
338
|
+
if (chars() > (o.maxContextChars ?? 160_000))
|
|
339
|
+
throw new Error("Context budget exceeded and compaction could not reduce it; start a new session.");
|
|
340
|
+
let completion;
|
|
341
|
+
let usage;
|
|
342
|
+
for await (const e of provider.stream({
|
|
343
|
+
instructions,
|
|
344
|
+
messages,
|
|
345
|
+
tools,
|
|
346
|
+
maxOutputTokens: o.maxOutputTokens ?? 4096,
|
|
347
|
+
signal: combined,
|
|
348
|
+
model,
|
|
349
|
+
sessionId,
|
|
350
|
+
...(o.nativeTools?.length ? { nativeTools: o.nativeTools } : {}),
|
|
351
|
+
})) {
|
|
352
|
+
combined.throwIfAborted();
|
|
353
|
+
if (e.type === "text_delta")
|
|
354
|
+
emit("text_delta", { delta: e.delta });
|
|
355
|
+
else if (e.type === "reasoning_delta")
|
|
356
|
+
emit("reasoning_delta", { delta: e.delta });
|
|
357
|
+
else {
|
|
358
|
+
if (completion)
|
|
359
|
+
throw new Error("Provider emitted multiple completions");
|
|
360
|
+
completion = e.message;
|
|
361
|
+
usage = e.usage;
|
|
362
|
+
tokens += (e.usage?.input ?? 0) + (e.usage?.output ?? 0);
|
|
363
|
+
usageTotal.input += e.usage?.input ?? 0;
|
|
364
|
+
usageTotal.output += e.usage?.output ?? 0;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (!completion)
|
|
368
|
+
throw new Error("Provider stream ended without a completed response");
|
|
369
|
+
const ids = completion.calls.map((c) => c.id);
|
|
370
|
+
const previousIds = new Set(messages.flatMap((m) => (m.role === "assistant" ? m.calls.map((c) => c.id) : [])));
|
|
371
|
+
if (ids.some((id) => previousIds.has(id)))
|
|
372
|
+
throw new Error("Provider reused a tool call ID across turns");
|
|
373
|
+
if (new Set(ids).size !== ids.length)
|
|
374
|
+
throw new Error("Provider returned duplicate tool call IDs");
|
|
375
|
+
if (completion.calls.length > 64)
|
|
376
|
+
throw new Error("Too many tool calls in one turn");
|
|
377
|
+
o.store.append(sessionId, completion);
|
|
378
|
+
if (usage)
|
|
379
|
+
this.reported.set(sessionId, {
|
|
380
|
+
tokens: usage.input + usage.output,
|
|
381
|
+
count: messages.length + 1,
|
|
382
|
+
});
|
|
383
|
+
lastText = completion.text;
|
|
384
|
+
emit("turn_completed", {
|
|
385
|
+
turn: turn + 1,
|
|
386
|
+
tokens,
|
|
387
|
+
calls: completion.calls.length,
|
|
388
|
+
model,
|
|
389
|
+
...(usage ? { usage } : {}),
|
|
390
|
+
});
|
|
391
|
+
if (!completion.calls.length) {
|
|
392
|
+
emit("run_completed", { tokens, text: lastText });
|
|
393
|
+
return { sessionId, text: lastText, status: "completed", usage: usageTotal };
|
|
394
|
+
}
|
|
395
|
+
const prepared = completion.calls.map((call) => {
|
|
396
|
+
try {
|
|
397
|
+
const tool = o.registry.get(call.name), input = o.registry.parse(call.name, call.arguments);
|
|
398
|
+
return { call, tool, input };
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
return { call, error: String(error) };
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
// Resolve scopes before tools execute. New nested instructions are attached to the first
|
|
405
|
+
// read result; state-changing calls return to the model for reconsideration instead.
|
|
406
|
+
let contextUpdate;
|
|
407
|
+
const paths = prepared.flatMap((p) => p.tool?.paths?.(p.input ?? {}) ?? []);
|
|
408
|
+
if (paths.length)
|
|
409
|
+
contextUpdate = await context.beforePaths(paths, sessionId);
|
|
410
|
+
let pendingUpdate = contextUpdate;
|
|
411
|
+
const execute = async (p) => {
|
|
412
|
+
const { call } = p;
|
|
413
|
+
let result;
|
|
414
|
+
const started = Date.now();
|
|
415
|
+
emit("tool_started", { name: call.name, id: call.id, arguments: call.arguments });
|
|
416
|
+
try {
|
|
417
|
+
combined.throwIfAborted();
|
|
418
|
+
if (p.error)
|
|
419
|
+
throw new Error(p.error);
|
|
420
|
+
if (!p.tool || !p.input)
|
|
421
|
+
throw new Error("Invalid tool");
|
|
422
|
+
const effect = p.tool.effect ?? "external";
|
|
423
|
+
if (contextUpdate && effect !== "read")
|
|
424
|
+
throw new Error(`Context changed; reconsider this call before retrying.\n${contextUpdate}`);
|
|
425
|
+
if (tokens >= budget)
|
|
426
|
+
throw new Error("Token budget exhausted; tool was not executed");
|
|
427
|
+
if (options.toolFilter && !options.toolFilter(p.tool))
|
|
428
|
+
throw new Error(`Tool ${call.name} is not available in this session`);
|
|
429
|
+
if (!allowed(policy, effect)) {
|
|
430
|
+
if (!o.approve ||
|
|
431
|
+
!approvals ||
|
|
432
|
+
(effect !== "write" && effect !== "process" && effect !== "external"))
|
|
433
|
+
throw new Error(`Capability denied: ${effect}`);
|
|
434
|
+
emit("approval_requested", {
|
|
435
|
+
id: call.id,
|
|
436
|
+
name: call.name,
|
|
437
|
+
effect,
|
|
438
|
+
...(options.label ? { label: options.label } : {}),
|
|
439
|
+
});
|
|
440
|
+
const decision = await o.approve({
|
|
441
|
+
call,
|
|
442
|
+
effect,
|
|
443
|
+
input: p.input,
|
|
444
|
+
signal: combined,
|
|
445
|
+
session: sessionId,
|
|
446
|
+
...(options.label ? { label: options.label } : {}),
|
|
447
|
+
});
|
|
448
|
+
combined.throwIfAborted();
|
|
449
|
+
emit("approval_resolved", { id: call.id, name: call.name, effect, decision });
|
|
450
|
+
if (decision === "deny")
|
|
451
|
+
throw new Error(`Capability ${effect} denied by the user for this call`);
|
|
452
|
+
if (decision === "session")
|
|
453
|
+
policy[effect] = true;
|
|
454
|
+
}
|
|
455
|
+
o.store.beginCall(sessionId, call);
|
|
456
|
+
result = await p.tool.execute(p.input, {
|
|
457
|
+
signal: combined,
|
|
458
|
+
workspace,
|
|
459
|
+
session: sessionId,
|
|
460
|
+
emit: (data) => emit("tool_progress", { id: call.id, data }),
|
|
461
|
+
...(options.label ? { label: options.label } : {}),
|
|
462
|
+
});
|
|
463
|
+
if (JSON.stringify(result).length > 48_000)
|
|
464
|
+
result = textResult(`${JSON.stringify(result).slice(0, 40_000)}\n[tool output truncated]`, result.isError);
|
|
465
|
+
if (pendingUpdate && effect === "read") {
|
|
466
|
+
result = {
|
|
467
|
+
...result,
|
|
468
|
+
content: [
|
|
469
|
+
...result.content,
|
|
470
|
+
{ type: "text", text: `\n<instructions>\n${pendingUpdate}\n</instructions>` },
|
|
471
|
+
],
|
|
472
|
+
};
|
|
473
|
+
pendingUpdate = undefined;
|
|
474
|
+
}
|
|
475
|
+
o.store.endCall(sessionId, call, result);
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
result = textResult(error instanceof Error ? error.message : String(error), true);
|
|
479
|
+
// A cancellation after execution may have left effects; journal remains pending.
|
|
480
|
+
if (!combined.aborted)
|
|
481
|
+
o.store.endCall(sessionId, call, result);
|
|
482
|
+
}
|
|
483
|
+
emit("tool_completed", {
|
|
484
|
+
id: call.id,
|
|
485
|
+
name: call.name,
|
|
486
|
+
isError: result.isError ?? false,
|
|
487
|
+
durationMs: Date.now() - started,
|
|
488
|
+
preview: result.content
|
|
489
|
+
.map((c) => c.text)
|
|
490
|
+
.join("\n")
|
|
491
|
+
.slice(0, 2_000),
|
|
492
|
+
});
|
|
493
|
+
return result;
|
|
494
|
+
};
|
|
495
|
+
const results = [];
|
|
496
|
+
// Bounded parallel batches for consecutive read or explicitly concurrent tools.
|
|
497
|
+
const parallel = (p) => !!p?.tool && (p.tool.effect === "read" || p.tool.concurrent === true);
|
|
498
|
+
for (let i = 0; i < prepared.length;) {
|
|
499
|
+
const current = prepared[i];
|
|
500
|
+
if (!current)
|
|
501
|
+
break;
|
|
502
|
+
if (parallel(current)) {
|
|
503
|
+
const batch = [];
|
|
504
|
+
while (i < prepared.length &&
|
|
505
|
+
parallel(prepared[i]) &&
|
|
506
|
+
batch.length < (o.readConcurrency ?? 4)) {
|
|
507
|
+
const p = prepared[i++];
|
|
508
|
+
if (p)
|
|
509
|
+
batch.push(p);
|
|
510
|
+
}
|
|
511
|
+
results.push(...(await Promise.all(batch.map(execute))));
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
results.push(await execute(current));
|
|
515
|
+
i++;
|
|
516
|
+
}
|
|
517
|
+
combined.throwIfAborted();
|
|
518
|
+
}
|
|
519
|
+
for (let i = 0; i < completion.calls.length; i++) {
|
|
520
|
+
const c = completion.calls[i], r = results[i];
|
|
521
|
+
if (c && r)
|
|
522
|
+
o.store.append(sessionId, { role: "tool", callId: c.id, result: r });
|
|
523
|
+
}
|
|
524
|
+
if (tokens >= budget)
|
|
525
|
+
throw new Error("Token budget exhausted");
|
|
526
|
+
}
|
|
527
|
+
throw new Error("Maximum turns reached");
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
// On cancellation report the abort reason, not the transport's secondary error.
|
|
531
|
+
const cause = combined.aborted ? (combined.reason ?? error) : error;
|
|
532
|
+
emit(combined.aborted ? "run_cancelled" : "run_failed", {
|
|
533
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
534
|
+
});
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
if (acquired)
|
|
539
|
+
o.store.release(sessionId);
|
|
540
|
+
this.active.delete(sessionId);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic, typed extension registry. Resolution is deterministic and independent of plugin
|
|
3
|
+
* load order: highest priority, then plugin id (lexicographic), then registration order
|
|
4
|
+
* within the plugin. Fallbacks (built-in defaults) only win when nothing else is registered.
|
|
5
|
+
*/
|
|
6
|
+
import type { ExtensionPoints } from "@alisio/sdk";
|
|
7
|
+
export interface ExtensionConflict {
|
|
8
|
+
point: string;
|
|
9
|
+
/** `plugin/provider` of the chosen provider. */
|
|
10
|
+
winner: string;
|
|
11
|
+
losers: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare class ExtensionRegistry {
|
|
14
|
+
private entries;
|
|
15
|
+
private seq;
|
|
16
|
+
register<K extends keyof ExtensionPoints>(point: K, provider: ExtensionPoints[K], options: {
|
|
17
|
+
plugin: string;
|
|
18
|
+
priority?: number;
|
|
19
|
+
fallback?: boolean;
|
|
20
|
+
}): () => void;
|
|
21
|
+
private ranked;
|
|
22
|
+
resolve<K extends keyof ExtensionPoints>(point: K): {
|
|
23
|
+
provider: ExtensionPoints[K];
|
|
24
|
+
plugin: string;
|
|
25
|
+
} | undefined;
|
|
26
|
+
/** Equal-priority, non-fallback competitors for each point's winning slot. */
|
|
27
|
+
conflicts(point?: keyof ExtensionPoints): ExtensionConflict[];
|
|
28
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const key = (e) => `${e.plugin}/${e.provider.id}`;
|
|
2
|
+
function compare(a, b) {
|
|
3
|
+
if (a.fallback !== b.fallback)
|
|
4
|
+
return a.fallback ? 1 : -1;
|
|
5
|
+
if (a.priority !== b.priority)
|
|
6
|
+
return b.priority - a.priority;
|
|
7
|
+
if (a.plugin !== b.plugin)
|
|
8
|
+
return a.plugin < b.plugin ? -1 : 1;
|
|
9
|
+
return a.seq - b.seq;
|
|
10
|
+
}
|
|
11
|
+
export class ExtensionRegistry {
|
|
12
|
+
entries = [];
|
|
13
|
+
seq = 0;
|
|
14
|
+
register(point, provider, options) {
|
|
15
|
+
// `id` is the one thing every extension point shares; a point-specific shape (e.g. `render`
|
|
16
|
+
// for a renderable point) is validated by that point's own resolution call site, the same way
|
|
17
|
+
// a throwing or malformed provider already falls back there (see `startup/index.ts`).
|
|
18
|
+
if (!provider || typeof provider.id !== "string")
|
|
19
|
+
throw new Error(`Invalid ${point} provider: it needs an id`);
|
|
20
|
+
const priority = options.priority ?? 0;
|
|
21
|
+
if (!Number.isFinite(priority))
|
|
22
|
+
throw new Error("Extension priority must be a finite number");
|
|
23
|
+
const entry = {
|
|
24
|
+
point,
|
|
25
|
+
provider,
|
|
26
|
+
plugin: options.plugin,
|
|
27
|
+
priority,
|
|
28
|
+
fallback: !!options.fallback,
|
|
29
|
+
seq: this.seq++,
|
|
30
|
+
};
|
|
31
|
+
this.entries.push(entry);
|
|
32
|
+
return () => {
|
|
33
|
+
this.entries = this.entries.filter((e) => e !== entry);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
ranked(point) {
|
|
37
|
+
return this.entries.filter((e) => e.point === point).sort(compare);
|
|
38
|
+
}
|
|
39
|
+
resolve(point) {
|
|
40
|
+
const top = this.ranked(point)[0];
|
|
41
|
+
return top ? { provider: top.provider, plugin: top.plugin } : undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Equal-priority, non-fallback competitors for each point's winning slot. */
|
|
44
|
+
conflicts(point) {
|
|
45
|
+
const points = point ? [point] : [...new Set(this.entries.map((e) => e.point))];
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const p of points) {
|
|
48
|
+
const ranked = this.ranked(p).filter((e) => !e.fallback);
|
|
49
|
+
const winner = ranked[0];
|
|
50
|
+
if (!winner)
|
|
51
|
+
continue;
|
|
52
|
+
const losers = ranked.filter((e) => e !== winner && e.priority === winner.priority);
|
|
53
|
+
if (losers.length)
|
|
54
|
+
out.push({ point: p, winner: key(winner), losers: losers.map(key) });
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** @alisio/core public API: embed the Alisio agent programmatically. */
|
|
2
|
+
export * from "@alisio/sdk";
|
|
3
|
+
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, type LoadedConfig, loadConfig, loadConfigWithProvenance, type McpServerSource, overridesSavedProviderProfile, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.ts";
|
|
5
|
+
export { checkpointInstructions, completeText, estimateTokens, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, } from "./core/compaction.ts";
|
|
6
|
+
export type { ApprovalDecision, ApprovalHandler, ApprovalRequest, ContextSource, HookFailure, Policy, RunnerExtensions, Session, SessionStore, } from "./core/contracts.ts";
|
|
7
|
+
export { ToolRegistry } from "./core/registry.ts";
|
|
8
|
+
export { AgentRunner, type CompactionResult, defaultTokenBudget, type RunnerOptions, type RunOptions, } from "./core/runner.ts";
|
|
9
|
+
export { type ExtensionConflict, ExtensionRegistry } from "./extensions/registry.ts";
|
|
10
|
+
export { HerdrBridge } from "./integrations/herdr.ts";
|
|
11
|
+
export { McpConnector, type McpServerInfo, type McpStatus, type McpToolInfo, } from "./mcp/connector.ts";
|
|
12
|
+
export { discoverPlugins, PluginHost, type PluginHostOptions, pluginPrefix, } from "./plugins/host.ts";
|
|
13
|
+
export { ActiveProvider, ProviderRegistry, UnconfiguredProvider } from "./providers/registry.ts";
|
|
14
|
+
export { availableProviderModels, type ProviderModelCatalog, resolveProviderModel, } from "./providers/routing.ts";
|
|
15
|
+
export { type ProviderProfile, type ProviderSettings, ProviderSettingsStore, } from "./providers/settings.ts";
|
|
16
|
+
export { type InstructionFile, ProjectContext, type ProjectContextOptions, } from "./resources/context.ts";
|
|
17
|
+
export { expandSlashPrompt, loadPromptTemplates, type PromptDiagnostic, type PromptRequirement, type PromptSource, type PromptSourceKind, type PromptTemplate, parsePromptTemplate, promptSources, renderPromptTemplate, splitArguments, } from "./resources/prompts.ts";
|
|
18
|
+
export { type Skill, type SkillCatalogEntry, type SkillRoot, type SkillScope, Skills, skillRoots, } from "./resources/skills.ts";
|
|
19
|
+
export { exists, fileSize, readHead, readJson, readText, which } from "./runtime/fs.ts";
|
|
20
|
+
export { defaultGlobalRoots, isPathSpec, PLUGIN_KEYWORD, resolvePluginSpec, } from "./runtime/modules.ts";
|
|
21
|
+
export { findWorkspace, safePath } from "./runtime/paths.ts";
|
|
22
|
+
export { type ProcessResult, runProcess } from "./runtime/process.ts";
|
|
23
|
+
export { isSqliteExperimentalWarning, openDatabase } from "./runtime/sqlite.ts";
|
|
24
|
+
export { SQLiteStore } from "./runtime/store.ts";
|
|
25
|
+
export { ChildSessions, type ChildSessionsOptions } from "./sessions/children.ts";
|
|
26
|
+
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";
|
|
27
|
+
export { hash, objectSchema, registerStandard } from "./tools/standard.ts";
|
|
28
|
+
export { getTrust, hashProjectConfig, hasProjectResources, listTrust, resolveTrust, revokeTrust, setTrust, type TrustEntry, type TrustListing, type TrustResolution, trustStorePath, } from "./trust.ts";
|