@alisio/core 0.1.0-alpha.17 → 0.1.0-alpha.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/active.d.ts +66 -0
- package/dist/agents/active.js +113 -0
- package/dist/application.d.ts +12 -3
- package/dist/application.js +25 -5
- package/dist/commands/builtins.d.ts +10 -0
- package/dist/commands/builtins.js +122 -0
- package/dist/commands/catalog.d.ts +86 -0
- package/dist/commands/catalog.js +292 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +27 -0
- package/dist/core/contracts.d.ts +109 -2
- package/dist/core/runner.d.ts +21 -0
- package/dist/core/runner.js +73 -12
- package/dist/index.d.ts +9 -4
- package/dist/index.js +8 -3
- package/dist/mcp/rich.js +68 -0
- package/dist/plugins/host.d.ts +4 -0
- package/dist/plugins/host.js +22 -8
- package/dist/providers/settings.d.ts +15 -0
- package/dist/providers/settings.js +38 -0
- package/dist/runtime/blobs.d.ts +29 -0
- package/dist/runtime/blobs.js +94 -0
- package/dist/runtime/diff.d.ts +23 -0
- package/dist/runtime/diff.js +154 -0
- package/dist/runtime/store.d.ts +75 -3
- package/dist/runtime/store.js +309 -11
- package/dist/sessions/side-questions.d.ts +57 -0
- package/dist/sessions/side-questions.js +161 -0
- package/dist/tools/standard.js +59 -9
- package/package.json +2 -2
package/dist/core/runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { textProjection, textResult, } from "@alisio/sdk";
|
|
1
|
+
import { isEphemeralRunEventType, textProjection, textResult, } from "@alisio/sdk";
|
|
2
2
|
import { checkpointInstructions, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, reduceMessagesToBudget, shouldCompactContext, summarize, summaryMessage, } from "./compaction.js";
|
|
3
3
|
/**
|
|
4
4
|
* Default cumulative token budget for one run: several context windows (each turn re-sends the
|
|
@@ -49,8 +49,9 @@ export class AgentRunner {
|
|
|
49
49
|
...(compaction ? { compaction: { ...this.options.compaction, ...compaction } } : {}),
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
emitter(sessionId) {
|
|
53
|
-
const runId = crypto.randomUUID();
|
|
52
|
+
emitter(sessionId, run = {}) {
|
|
53
|
+
const runId = run.runId ?? crypto.randomUUID();
|
|
54
|
+
const correlationId = run.correlationId;
|
|
54
55
|
let seq = 0;
|
|
55
56
|
return (type, data) => {
|
|
56
57
|
const event = {
|
|
@@ -62,8 +63,15 @@ export class AgentRunner {
|
|
|
62
63
|
timestamp: new Date().toISOString(),
|
|
63
64
|
data,
|
|
64
65
|
};
|
|
65
|
-
if (type
|
|
66
|
-
|
|
66
|
+
if (!isEphemeralRunEventType(type)) {
|
|
67
|
+
const persisted = correlationId !== undefined
|
|
68
|
+
? this.options.store.event(sessionId, runId, type, data, { correlationId })
|
|
69
|
+
: this.options.store.event(sessionId, runId, type, data);
|
|
70
|
+
if (typeof persisted === "number")
|
|
71
|
+
event.eventId = String(persisted);
|
|
72
|
+
}
|
|
73
|
+
if (correlationId !== undefined)
|
|
74
|
+
event.correlationId = correlationId;
|
|
67
75
|
try {
|
|
68
76
|
this.options.onEvent?.(event);
|
|
69
77
|
}
|
|
@@ -147,6 +155,30 @@ export class AgentRunner {
|
|
|
147
155
|
};
|
|
148
156
|
return { total: undefined, basis: "unknown", compactionAt: 100 };
|
|
149
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
|
+
}
|
|
150
182
|
/** Change the model used by subsequent turns of a session; recorded in the store. */
|
|
151
183
|
setModel(sessionId, model) {
|
|
152
184
|
const id = model.trim();
|
|
@@ -280,7 +312,17 @@ export class AgentRunner {
|
|
|
280
312
|
}
|
|
281
313
|
async run(sessionId, prompt, signal, options = {}) {
|
|
282
314
|
const controller = this.claim(sessionId);
|
|
283
|
-
const
|
|
315
|
+
const runId = options.runId ?? crypto.randomUUID();
|
|
316
|
+
const o = this.options, emit = this.emitter(sessionId, { runId, correlationId: options.correlationId });
|
|
317
|
+
/** Run journal (v4 stores only): an auxiliary record that never changes the run outcome. */
|
|
318
|
+
const finish = (status, error) => {
|
|
319
|
+
try {
|
|
320
|
+
o.store.endRun?.(runId, { status, usage: usageTotal, ...(error ? { error } : {}) });
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
/* A journal write failure cannot invalidate an executed run. */
|
|
324
|
+
}
|
|
325
|
+
};
|
|
284
326
|
const limit = o.maxContextChars ?? 800_000;
|
|
285
327
|
let tokens = 0, lastText = "", acquired = false;
|
|
286
328
|
const usageTotal = { input: 0, output: 0 };
|
|
@@ -296,6 +338,13 @@ export class AgentRunner {
|
|
|
296
338
|
return options.instructions ? `${base}\n\n${options.instructions}` : base;
|
|
297
339
|
};
|
|
298
340
|
try {
|
|
341
|
+
o.store.beginRun?.({
|
|
342
|
+
id: runId,
|
|
343
|
+
session: sessionId,
|
|
344
|
+
status: "running",
|
|
345
|
+
model: o.store.get(sessionId).model,
|
|
346
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
347
|
+
});
|
|
299
348
|
o.store.acquire(sessionId);
|
|
300
349
|
acquired = true;
|
|
301
350
|
o.store.reconcile(sessionId);
|
|
@@ -414,6 +463,8 @@ export class AgentRunner {
|
|
|
414
463
|
// display-only extension of the persisted transcript and never reach a model prompt
|
|
415
464
|
// (raw image bytes included). The store keeps the rich parts for TUI replay.
|
|
416
465
|
const providerMessages = messages.map((m) => m.role === "tool" ? { ...m, result: textProjection(m.result) } : m);
|
|
466
|
+
const requested = Date.now();
|
|
467
|
+
let firstDelta;
|
|
417
468
|
for await (const e of provider.stream({
|
|
418
469
|
instructions,
|
|
419
470
|
messages: providerMessages,
|
|
@@ -428,6 +479,8 @@ export class AgentRunner {
|
|
|
428
479
|
: {}),
|
|
429
480
|
})) {
|
|
430
481
|
combined.throwIfAborted();
|
|
482
|
+
if (e.type !== "completed")
|
|
483
|
+
firstDelta ??= Date.now();
|
|
431
484
|
if (e.type === "text_delta")
|
|
432
485
|
emit("text_delta", { delta: e.delta });
|
|
433
486
|
else if (e.type === "reasoning_delta")
|
|
@@ -445,6 +498,7 @@ export class AgentRunner {
|
|
|
445
498
|
}
|
|
446
499
|
if (!completion)
|
|
447
500
|
throw new Error("Provider stream ended without a completed response");
|
|
501
|
+
const durationMs = Date.now() - requested;
|
|
448
502
|
const ids = completion.calls.map((c) => c.id);
|
|
449
503
|
const previousIds = new Set(messages.flatMap((m) => (m.role === "assistant" ? m.calls.map((c) => c.id) : [])));
|
|
450
504
|
if (ids.some((id) => previousIds.has(id)))
|
|
@@ -466,6 +520,8 @@ export class AgentRunner {
|
|
|
466
520
|
calls: completion.calls.length,
|
|
467
521
|
model,
|
|
468
522
|
...(usage ? { usage } : {}),
|
|
523
|
+
durationMs,
|
|
524
|
+
...(firstDelta !== undefined ? { ttftMs: firstDelta - requested } : {}),
|
|
469
525
|
});
|
|
470
526
|
if (!completion.calls.length) {
|
|
471
527
|
const final = {
|
|
@@ -481,6 +537,7 @@ export class AgentRunner {
|
|
|
481
537
|
final.truncated = true;
|
|
482
538
|
}
|
|
483
539
|
emit("run_completed", final);
|
|
540
|
+
finish("completed");
|
|
484
541
|
return { sessionId, text: lastText, status: "completed", usage: usageTotal };
|
|
485
542
|
}
|
|
486
543
|
const prepared = completion.calls.map((call) => {
|
|
@@ -571,7 +628,7 @@ export class AgentRunner {
|
|
|
571
628
|
if (decision === "session")
|
|
572
629
|
policy[effect] = true;
|
|
573
630
|
}
|
|
574
|
-
o.store.beginCall(sessionId, call);
|
|
631
|
+
o.store.beginCall(sessionId, call, { runId, effect });
|
|
575
632
|
result = await p.tool.execute(p.input, {
|
|
576
633
|
signal: combined,
|
|
577
634
|
workspace,
|
|
@@ -594,8 +651,11 @@ export class AgentRunner {
|
|
|
594
651
|
}
|
|
595
652
|
: {}),
|
|
596
653
|
});
|
|
597
|
-
|
|
598
|
-
|
|
654
|
+
// The bound applies to what the model sees (the text projection): display-only
|
|
655
|
+
// ui/image parts are bounded by their producers and must not push text over it.
|
|
656
|
+
const projected = textProjection(result);
|
|
657
|
+
if (JSON.stringify(projected).length > 48_000)
|
|
658
|
+
result = textResult(`${JSON.stringify(projected).slice(0, 40_000)}\n[tool output truncated]`, result.isError);
|
|
599
659
|
if (pendingUpdate && effect === "read") {
|
|
600
660
|
result = {
|
|
601
661
|
...result,
|
|
@@ -664,14 +724,15 @@ export class AgentRunner {
|
|
|
664
724
|
// can simply prompt again to continue in the same session. The real hard stops remain the
|
|
665
725
|
// token budget (`maxTokens`) and the timeout; the turn count is a safety rail.
|
|
666
726
|
emit("run_turns_exceeded", { turns: maxTurns, maxTurns });
|
|
727
|
+
finish("turns_exceeded");
|
|
667
728
|
return { sessionId, text: lastText, status: "turns-exceeded", usage: usageTotal };
|
|
668
729
|
}
|
|
669
730
|
catch (error) {
|
|
670
731
|
// On cancellation report the abort reason, not the transport's secondary error.
|
|
671
732
|
const cause = combined.aborted ? (combined.reason ?? error) : error;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
733
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
734
|
+
emit(combined.aborted ? "run_cancelled" : "run_failed", { error: message });
|
|
735
|
+
finish(combined.aborted ? "cancelled" : "failed", message);
|
|
675
736
|
throw error;
|
|
676
737
|
}
|
|
677
738
|
finally {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/** @alisio/core public API: embed the Alisio agent programmatically. */
|
|
2
2
|
export * from "@alisio/sdk";
|
|
3
|
+
export { type ActiveAgent, type AgentPickerItem, activeAgentCatalog, agentCatalogFromState, agentPickerItems, agentRunOptions, BUILTIN_AGENTS, DEFAULT_AGENT_ID, type MainCapableAgentRecord, mainAgentFromRecord, resolveActiveAgent, } from "./agents/active.ts";
|
|
3
4
|
export { type AppOptions, type BuiltinContext, type BuiltinPlugin, createApplication, enabledBuiltins, type PluginCatalogEntry, type PluginCatalogStatus, } from "./application.ts";
|
|
4
|
-
export {
|
|
5
|
+
export { BUILTIN_COMMANDS, type BuiltinCommand, CommandCatalog, type CommandExecutionContext, type CommandHost, type CommandResult, type CommandSurface, } from "./commands/catalog.ts";
|
|
6
|
+
export { type Config, type ConfigLoadResult, type ConfigProvenance, configFile, configHome, configSchema, isSettableSettingKey, type LoadedConfig, loadConfig, loadConfigWithProvenance, type McpServerSource, overridesSavedProviderProfile, type SettableSettingInfo, type SettableSettingKey, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, settableSettings, stateHome, } from "./config.ts";
|
|
5
7
|
export { type ContextBudget, checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.ts";
|
|
6
|
-
export type { ApprovalDecision, ApprovalHandler, ApprovalRequest, ContextSource, HookFailure, Policy, RunnerExtensions, Session, SessionStore, } from "./core/contracts.ts";
|
|
8
|
+
export type { ApprovalDecision, ApprovalHandler, ApprovalRequest, BeginRunInput, ContextSource, EndRunInput, EventPage, HookFailure, MessagePage, PageOptions, Policy, RunnerExtensions, RunRecord, RunStatus, Session, SessionStore, StoredEvent, TerminalRunStatus, ToolCallMeta, } from "./core/contracts.ts";
|
|
7
9
|
export { ToolRegistry } from "./core/registry.ts";
|
|
8
10
|
export { AgentRunner, type CompactionResult, defaultTokenBudget, type RunnerOptions, type RunnerSettingsPatch, type RunOptions, } from "./core/runner.ts";
|
|
9
11
|
export { type ExtensionConflict, ExtensionRegistry } from "./extensions/registry.ts";
|
|
@@ -13,18 +15,21 @@ export { discoverPlugins, PluginHost, type PluginHostOptions, pluginPrefix, } fr
|
|
|
13
15
|
export { type CliInstallOptions, cliInstall, INSTALL_TIMEOUT_MS, type InstallPluginInput, type InstallPluginResult, type InstallRunner, installedNpmPlugins, installPlugin, npmInstallTarget, type PluginSpec, parsePluginSpec, pluginTrustRemark, printInstallResult, registerPluginInstallTool, sanitizeNpmError, } from "./plugins/install.ts";
|
|
14
16
|
export { ActiveProvider, ProviderRegistry, UnconfiguredProvider } from "./providers/registry.ts";
|
|
15
17
|
export { availableProviderModels, type ProviderModelCatalog, resolveProviderModel, } from "./providers/routing.ts";
|
|
16
|
-
export { type ProviderProfile, type ProviderSettings, ProviderSettingsStore, } from "./providers/settings.ts";
|
|
18
|
+
export { maskSecret, type ProviderProfile, type ProviderSettings, ProviderSettingsStore, } from "./providers/settings.ts";
|
|
17
19
|
export { type InstructionFile, ProjectContext, type ProjectContextOptions, } from "./resources/context.ts";
|
|
18
20
|
export { expandSlashPrompt, loadPromptTemplates, type PromptDiagnostic, type PromptRequirement, type PromptSource, type PromptSourceKind, type PromptTemplate, parsePromptTemplate, promptSources, renderPromptTemplate, splitArguments, } from "./resources/prompts.ts";
|
|
19
21
|
export { type Skill, type SkillCatalogEntry, type SkillRoot, type SkillScope, Skills, skillRoots, } from "./resources/skills.ts";
|
|
20
22
|
export { type ExternalDirectoryDecision, type ExternalDirectoryHandler, type ExternalDirectoryRequest, PathAccess, type PathAccessOptions, type ResolvePathOptions, } from "./runtime/access.ts";
|
|
23
|
+
export { BlobStore, type BlobStoreOptions } from "./runtime/blobs.ts";
|
|
24
|
+
export { clipLines, unifiedPatch } from "./runtime/diff.ts";
|
|
21
25
|
export { exists, fileSize, readHead, readJson, readText, which } from "./runtime/fs.ts";
|
|
22
26
|
export { defaultGlobalRoots, isPathSpec, PLUGIN_KEYWORD, resolvePluginSpec, } from "./runtime/modules.ts";
|
|
23
|
-
export { findWorkspace, outsideRootsMessage, safePath } from "./runtime/paths.ts";
|
|
27
|
+
export { findWorkspace, inside, outsideRootsMessage, safePath } from "./runtime/paths.ts";
|
|
24
28
|
export { isMissingCommand, type ProcessResult, RIPGREP_INSTALL_HINT, runProcess, } from "./runtime/process.ts";
|
|
25
29
|
export { isSqliteExperimentalWarning, openDatabase } from "./runtime/sqlite.ts";
|
|
26
30
|
export { SQLiteStore } from "./runtime/store.ts";
|
|
27
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";
|
|
28
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";
|
|
29
34
|
export { hash, objectSchema, registerStandard } from "./tools/standard.ts";
|
|
30
35
|
export { getTrust, hashProjectConfig, hasProjectResources, listTrust, resolveTrust, revokeTrust, setTrust, type TrustEntry, type TrustListing, type TrustResolution, trustStorePath, } from "./trust.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** @alisio/core public API: embed the Alisio agent programmatically. */
|
|
2
2
|
export * from "@alisio/sdk";
|
|
3
|
+
export { activeAgentCatalog, agentCatalogFromState, agentPickerItems, agentRunOptions, BUILTIN_AGENTS, DEFAULT_AGENT_ID, mainAgentFromRecord, resolveActiveAgent, } from "./agents/active.js";
|
|
3
4
|
export { createApplication, enabledBuiltins, } from "./application.js";
|
|
4
|
-
export {
|
|
5
|
+
export { BUILTIN_COMMANDS, CommandCatalog, } from "./commands/catalog.js";
|
|
6
|
+
export { configFile, configHome, configSchema, isSettableSettingKey, loadConfig, loadConfigWithProvenance, overridesSavedProviderProfile, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, settableSettings, stateHome, } from "./config.js";
|
|
5
7
|
export { checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.js";
|
|
6
8
|
export { ToolRegistry } from "./core/registry.js";
|
|
7
9
|
export { AgentRunner, defaultTokenBudget, } from "./core/runner.js";
|
|
@@ -12,18 +14,21 @@ export { discoverPlugins, PluginHost, pluginPrefix, } from "./plugins/host.js";
|
|
|
12
14
|
export { cliInstall, INSTALL_TIMEOUT_MS, installedNpmPlugins, installPlugin, npmInstallTarget, parsePluginSpec, pluginTrustRemark, printInstallResult, registerPluginInstallTool, sanitizeNpmError, } from "./plugins/install.js";
|
|
13
15
|
export { ActiveProvider, ProviderRegistry, UnconfiguredProvider } from "./providers/registry.js";
|
|
14
16
|
export { availableProviderModels, resolveProviderModel, } from "./providers/routing.js";
|
|
15
|
-
export { ProviderSettingsStore, } from "./providers/settings.js";
|
|
17
|
+
export { maskSecret, ProviderSettingsStore, } from "./providers/settings.js";
|
|
16
18
|
export { ProjectContext, } from "./resources/context.js";
|
|
17
19
|
export { expandSlashPrompt, loadPromptTemplates, parsePromptTemplate, promptSources, renderPromptTemplate, splitArguments, } from "./resources/prompts.js";
|
|
18
20
|
export { Skills, skillRoots, } from "./resources/skills.js";
|
|
19
21
|
export { PathAccess, } from "./runtime/access.js";
|
|
22
|
+
export { BlobStore } from "./runtime/blobs.js";
|
|
23
|
+
export { clipLines, unifiedPatch } from "./runtime/diff.js";
|
|
20
24
|
export { exists, fileSize, readHead, readJson, readText, which } from "./runtime/fs.js";
|
|
21
25
|
export { defaultGlobalRoots, isPathSpec, PLUGIN_KEYWORD, resolvePluginSpec, } from "./runtime/modules.js";
|
|
22
|
-
export { findWorkspace, outsideRootsMessage, safePath } from "./runtime/paths.js";
|
|
26
|
+
export { findWorkspace, inside, outsideRootsMessage, safePath } from "./runtime/paths.js";
|
|
23
27
|
export { isMissingCommand, RIPGREP_INSTALL_HINT, runProcess, } from "./runtime/process.js";
|
|
24
28
|
export { isSqliteExperimentalWarning, openDatabase } from "./runtime/sqlite.js";
|
|
25
29
|
export { SQLiteStore } from "./runtime/store.js";
|
|
26
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";
|
|
27
32
|
export { composeSideBySide, DefaultAlisioMascot, DefaultStartupScreen, infoSection, mascotSection, padEnd, pluginsSection, renderStartup, sanitizeLine, shortenPath, startupTips, stripAnsi, tipsSection, titleSection, truncate, visibleWidth, welcomeSection, } from "./startup/index.js";
|
|
28
33
|
export { hash, objectSchema, registerStandard } from "./tools/standard.js";
|
|
29
34
|
export { getTrust, hashProjectConfig, hasProjectResources, listTrust, resolveTrust, revokeTrust, setTrust, trustStorePath, } from "./trust.js";
|
package/dist/mcp/rich.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* `JSON.stringify(result)` behavior verbatim.
|
|
17
17
|
*/
|
|
18
18
|
import { textResult } from "@alisio/sdk";
|
|
19
|
+
import { stripAnsi } from "../startup/text.js";
|
|
19
20
|
const scalar = (value) => typeof value === "string"
|
|
20
21
|
? value
|
|
21
22
|
: typeof value === "number" || typeof value === "boolean" || value === null
|
|
@@ -177,6 +178,73 @@ export function renderUiBlockText(block) {
|
|
|
177
178
|
}
|
|
178
179
|
case "markdown":
|
|
179
180
|
return block.text;
|
|
181
|
+
case "diff": {
|
|
182
|
+
const label = [block.caption, block.path].filter(Boolean).join(" · ");
|
|
183
|
+
const head = label ? `[diff: ${label}]` : "[diff]";
|
|
184
|
+
if (block.patch !== undefined)
|
|
185
|
+
return `${head}\n${fenced(block.patch, "diff")}`;
|
|
186
|
+
return [
|
|
187
|
+
head,
|
|
188
|
+
`before:\n${fenced(block.before ?? "", block.lang)}`,
|
|
189
|
+
`after:\n${fenced(block.after ?? "", block.lang)}`,
|
|
190
|
+
].join("\n");
|
|
191
|
+
}
|
|
192
|
+
case "terminal": {
|
|
193
|
+
const status = [
|
|
194
|
+
block.exitCode !== undefined ? `exit ${block.exitCode}` : "",
|
|
195
|
+
block.durationMs !== undefined ? `${block.durationMs}ms` : "",
|
|
196
|
+
block.truncated ? "output truncated" : "",
|
|
197
|
+
]
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
.join(" · ");
|
|
200
|
+
return [
|
|
201
|
+
block.command ? `$ ${block.command}` : "[terminal]",
|
|
202
|
+
fenced(stripAnsi(block.output)),
|
|
203
|
+
status,
|
|
204
|
+
]
|
|
205
|
+
.filter((line) => line !== "")
|
|
206
|
+
.join("\n");
|
|
207
|
+
}
|
|
208
|
+
case "mermaid":
|
|
209
|
+
return [block.title ? `[mermaid: ${block.title}]` : "", fenced(block.source, "mermaid")]
|
|
210
|
+
.filter((line) => line !== "")
|
|
211
|
+
.join("\n");
|
|
212
|
+
case "math":
|
|
213
|
+
return block.display ? `$$\n${block.latex}\n$$` : `$${block.latex}$`;
|
|
214
|
+
case "json":
|
|
215
|
+
return [
|
|
216
|
+
block.caption ? `[json: ${block.caption}]` : "",
|
|
217
|
+
fenced(jsonText(block.value), "json"),
|
|
218
|
+
]
|
|
219
|
+
.filter((line) => line !== "")
|
|
220
|
+
.join("\n");
|
|
221
|
+
case "test-results": {
|
|
222
|
+
const cases = block.suites.flatMap((suite) => suite.cases.map((c) => `${c.status.toUpperCase()} ${suite.name} › ${c.name}${c.error ? `: ${c.error}` : ""}`));
|
|
223
|
+
return [`[test-results${block.framework ? `: ${block.framework}` : ""}]`, ...cases].join("\n");
|
|
224
|
+
}
|
|
225
|
+
case "progress":
|
|
226
|
+
return [
|
|
227
|
+
block.title ? `[progress: ${block.title}]` : "[progress]",
|
|
228
|
+
...block.steps.map((step) => `- [${step.status}] ${step.label}${step.detail ? ` (${step.detail})` : ""}`),
|
|
229
|
+
].join("\n");
|
|
230
|
+
default: {
|
|
231
|
+
// A kind this build does not know (e.g. persisted by a newer Alisio): keep it readable.
|
|
232
|
+
const kind = String(block.kind ?? "unknown");
|
|
233
|
+
return `[ui block: ${kind}]\n${fenced(jsonText(block), "json")}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function fenced(text, lang) {
|
|
238
|
+
const body = text.endsWith("\n") ? text : `${text}\n`;
|
|
239
|
+
return `\`\`\`${lang ?? ""}\n${body}\`\`\``;
|
|
240
|
+
}
|
|
241
|
+
/** `JSON.stringify` that never throws and never returns `undefined`. */
|
|
242
|
+
function jsonText(value) {
|
|
243
|
+
try {
|
|
244
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return String(value);
|
|
180
248
|
}
|
|
181
249
|
}
|
|
182
250
|
const base64Bytes = (data) => Math.floor((data.length * 3) / 4);
|
package/dist/plugins/host.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
37
37
|
private startHandlers;
|
|
38
38
|
private endHandlers;
|
|
39
39
|
private fieldOwners;
|
|
40
|
+
/** Registered tool name → contributing plugin id. */
|
|
41
|
+
private toolOwners;
|
|
40
42
|
private completer?;
|
|
41
43
|
private modelsImpl?;
|
|
42
44
|
observers: Set<(event: Readonly<RunEvent>) => void>;
|
|
@@ -166,6 +168,8 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
166
168
|
}>;
|
|
167
169
|
/** Loaded plugins (id, version, built-in flag), ordered by id. */
|
|
168
170
|
metadata(): PluginMetadata[];
|
|
171
|
+
/** Registered names of the tools a plugin contributes (namespaced unless built in). */
|
|
172
|
+
toolsOf(plugin: string): string[];
|
|
169
173
|
get hasSessionEndHooks(): boolean;
|
|
170
174
|
/** Conservative ownership check used to protect a live session from plugin removal. */
|
|
171
175
|
ownsSessionResources(plugin: string): boolean;
|
package/dist/plugins/host.js
CHANGED
|
@@ -38,6 +38,8 @@ export class PluginHost {
|
|
|
38
38
|
startHandlers = [];
|
|
39
39
|
endHandlers = [];
|
|
40
40
|
fieldOwners = new Map();
|
|
41
|
+
/** Registered tool name → contributing plugin id. */
|
|
42
|
+
toolOwners = new Map();
|
|
41
43
|
completer;
|
|
42
44
|
modelsImpl;
|
|
43
45
|
observers = new Set();
|
|
@@ -198,14 +200,22 @@ export class PluginHost {
|
|
|
198
200
|
};
|
|
199
201
|
const api = {
|
|
200
202
|
tools: {
|
|
201
|
-
register: (tool) =>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
register: (tool) => {
|
|
204
|
+
const registered = builtin
|
|
205
|
+
? tool
|
|
206
|
+
: {
|
|
207
|
+
...tool,
|
|
208
|
+
name: `${pluginPrefix(plugin.id)}_${tool.name}`,
|
|
209
|
+
// Only built-ins may claim the always-allowed internal effect.
|
|
210
|
+
...(tool.effect === "internal" ? { effect: "external" } : {}),
|
|
211
|
+
};
|
|
212
|
+
const unregister = this.registry.register(registered);
|
|
213
|
+
this.toolOwners.set(registered.name, plugin.id);
|
|
214
|
+
return track(() => {
|
|
215
|
+
unregister();
|
|
216
|
+
this.toolOwners.delete(registered.name);
|
|
217
|
+
});
|
|
218
|
+
},
|
|
209
219
|
},
|
|
210
220
|
commands: {
|
|
211
221
|
register: (name, handler, info) => {
|
|
@@ -544,6 +554,10 @@ export class PluginHost {
|
|
|
544
554
|
}))
|
|
545
555
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
546
556
|
}
|
|
557
|
+
/** Registered names of the tools a plugin contributes (namespaced unless built in). */
|
|
558
|
+
toolsOf(plugin) {
|
|
559
|
+
return [...this.toolOwners].filter(([, owner]) => owner === plugin).map(([name]) => name);
|
|
560
|
+
}
|
|
547
561
|
get hasSessionEndHooks() {
|
|
548
562
|
return this.endHandlers.length > 0;
|
|
549
563
|
}
|
|
@@ -9,6 +9,11 @@ export interface ProviderSettings {
|
|
|
9
9
|
active?: string;
|
|
10
10
|
profiles: Record<string, ProviderProfile>;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Display form of a secret: its last three characters behind an ellipsis, and nothing at all for
|
|
14
|
+
* secrets shorter than 16 characters (a tail would reveal too much of them).
|
|
15
|
+
*/
|
|
16
|
+
export declare function maskSecret(secret: string): string | undefined;
|
|
12
17
|
export declare class ProviderSettingsStore {
|
|
13
18
|
readonly profilesPath: string;
|
|
14
19
|
readonly credentialsPath: string;
|
|
@@ -27,4 +32,14 @@ export declare class ProviderSettingsStore {
|
|
|
27
32
|
credentials: Record<string, string>;
|
|
28
33
|
} | undefined>;
|
|
29
34
|
saveActive(name: string, profile: ProviderProfile, secrets: Record<string, string>): Promise<void>;
|
|
35
|
+
/** Stores or replaces a profile's non-secret values without changing the active profile. */
|
|
36
|
+
saveProfile(name: string, profile: ProviderProfile): Promise<void>;
|
|
37
|
+
/** Merges secrets into a profile's stored credentials (write-only: nothing is returned). */
|
|
38
|
+
setCredentials(name: string, secrets: Record<string, string>): Promise<void>;
|
|
39
|
+
/** Removes every stored credential of a profile; false when it had none. */
|
|
40
|
+
deleteCredentials(name: string): Promise<boolean>;
|
|
41
|
+
/** Which credentials a profile has stored, masked (`maskSecret`); never the values. */
|
|
42
|
+
credentialStatus(name: string): Promise<Record<string, {
|
|
43
|
+
tail?: string;
|
|
44
|
+
}>>;
|
|
30
45
|
}
|
|
@@ -29,6 +29,13 @@ async function atomicJson(path, value, mode) {
|
|
|
29
29
|
await rename(temporary, path);
|
|
30
30
|
await chmod(path, mode);
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Display form of a secret: its last three characters behind an ellipsis, and nothing at all for
|
|
34
|
+
* secrets shorter than 16 characters (a tail would reveal too much of them).
|
|
35
|
+
*/
|
|
36
|
+
export function maskSecret(secret) {
|
|
37
|
+
return secret.length >= 16 ? `…${secret.slice(-3)}` : undefined;
|
|
38
|
+
}
|
|
32
39
|
export class ProviderSettingsStore {
|
|
33
40
|
profilesPath;
|
|
34
41
|
credentialsPath;
|
|
@@ -77,4 +84,35 @@ export class ProviderSettingsStore {
|
|
|
77
84
|
await atomicJson(this.credentialsPath, credentials, 0o600);
|
|
78
85
|
await atomicJson(this.profilesPath, settings, 0o600);
|
|
79
86
|
}
|
|
87
|
+
/** Stores or replaces a profile's non-secret values without changing the active profile. */
|
|
88
|
+
async saveProfile(name, profile) {
|
|
89
|
+
const settings = await this.load();
|
|
90
|
+
settings.profiles[name] = profile;
|
|
91
|
+
await atomicJson(this.profilesPath, settings, 0o600);
|
|
92
|
+
}
|
|
93
|
+
/** Merges secrets into a profile's stored credentials (write-only: nothing is returned). */
|
|
94
|
+
async setCredentials(name, secrets) {
|
|
95
|
+
const credentials = await this.credentials();
|
|
96
|
+
credentials.providers[name] = { ...(credentials.providers[name] ?? {}), ...secrets };
|
|
97
|
+
await atomicJson(this.credentialsPath, credentials, 0o600);
|
|
98
|
+
}
|
|
99
|
+
/** Removes every stored credential of a profile; false when it had none. */
|
|
100
|
+
async deleteCredentials(name) {
|
|
101
|
+
const credentials = await this.credentials();
|
|
102
|
+
if (!credentials.providers[name] || !Object.keys(credentials.providers[name]).length)
|
|
103
|
+
return false;
|
|
104
|
+
delete credentials.providers[name];
|
|
105
|
+
await atomicJson(this.credentialsPath, credentials, 0o600);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
/** Which credentials a profile has stored, masked (`maskSecret`); never the values. */
|
|
109
|
+
async credentialStatus(name) {
|
|
110
|
+
const stored = (await this.credentials()).providers[name] ?? {};
|
|
111
|
+
return Object.fromEntries(Object.entries(stored)
|
|
112
|
+
.filter(([, value]) => typeof value === "string" && value.length > 0)
|
|
113
|
+
.map(([key, value]) => {
|
|
114
|
+
const tail = maskSecret(value);
|
|
115
|
+
return [key, tail ? { tail } : {}];
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
80
118
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Attachment, BlobRef, SqlDatabase } from "@alisio/sdk";
|
|
2
|
+
export interface BlobStoreOptions {
|
|
3
|
+
/** Directory that holds `sha256/…` (by default `<state home>/blobs`). */
|
|
4
|
+
root: string;
|
|
5
|
+
/** Session database with the v4 `blobs` table (e.g. `SQLiteStore.db`). */
|
|
6
|
+
db: SqlDatabase;
|
|
7
|
+
}
|
|
8
|
+
export declare class BlobStore {
|
|
9
|
+
private readonly options;
|
|
10
|
+
constructor(options: BlobStoreOptions);
|
|
11
|
+
/** Absolute file path of a blob. Throws for anything that is not a lowercase sha256 hex. */
|
|
12
|
+
path(hash: string): string;
|
|
13
|
+
has(hash: string): boolean;
|
|
14
|
+
private row;
|
|
15
|
+
/** Stores bytes once per content hash and returns their reference. */
|
|
16
|
+
put(bytes: Uint8Array, mimeType: string, dims?: {
|
|
17
|
+
width?: number;
|
|
18
|
+
height?: number;
|
|
19
|
+
}): BlobRef;
|
|
20
|
+
get(hash: string): {
|
|
21
|
+
bytes: Buffer;
|
|
22
|
+
mimeType: string;
|
|
23
|
+
} | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Resolves a reference into a provider-ready image `Attachment` (base64 `data`), verifying
|
|
26
|
+
* that the stored bytes still match the hash.
|
|
27
|
+
*/
|
|
28
|
+
attachment(ref: BlobRef): Attachment;
|
|
29
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-addressed attachment bytes (v4). Files live at `<root>/sha256/<hash[0..2]>/<hash>`
|
|
3
|
+
* (directories 0700, files 0600, atomic temp-file + rename); metadata lives in the `blobs` table
|
|
4
|
+
* of the session database. Portable `node:fs`/`node:crypto` only (Node and Bun).
|
|
5
|
+
*
|
|
6
|
+
* `Attachment.data` stays required in the SDK, so blobs never reach the runner by reference: the
|
|
7
|
+
* host resolves a `BlobRef` into a base64 `Attachment` with `attachment()` before `runner.run`.
|
|
8
|
+
* Persisted messages therefore still carry base64 (as legacy inline attachments do).
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
const HASH = /^[0-9a-f]{64}$/;
|
|
14
|
+
export class BlobStore {
|
|
15
|
+
options;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
/** Absolute file path of a blob. Throws for anything that is not a lowercase sha256 hex. */
|
|
20
|
+
path(hash) {
|
|
21
|
+
if (!HASH.test(hash))
|
|
22
|
+
throw new Error(`Invalid blob hash: ${hash.slice(0, 80)}`);
|
|
23
|
+
return join(this.options.root, "sha256", hash.slice(0, 2), hash);
|
|
24
|
+
}
|
|
25
|
+
has(hash) {
|
|
26
|
+
return HASH.test(hash) && existsSync(this.path(hash)) && !!this.row(hash);
|
|
27
|
+
}
|
|
28
|
+
row(hash) {
|
|
29
|
+
return this.options.db.prepare("SELECT * FROM blobs WHERE hash=?").get(hash);
|
|
30
|
+
}
|
|
31
|
+
/** Stores bytes once per content hash and returns their reference. */
|
|
32
|
+
put(bytes, mimeType, dims = {}) {
|
|
33
|
+
const mime = mimeType.trim();
|
|
34
|
+
if (!mime || mime.length > 255)
|
|
35
|
+
throw new Error("A blob needs a MIME type");
|
|
36
|
+
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
37
|
+
const file = this.path(hash);
|
|
38
|
+
if (!existsSync(file)) {
|
|
39
|
+
const dir = join(this.options.root, "sha256", hash.slice(0, 2));
|
|
40
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
41
|
+
chmodSync(dir, 0o700);
|
|
42
|
+
const temp = join(dir, `.tmp-${randomUUID()}`);
|
|
43
|
+
try {
|
|
44
|
+
writeFileSync(temp, bytes, { mode: 0o600, flag: "wx" });
|
|
45
|
+
renameSync(temp, file);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
rmSync(temp, { force: true });
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.options.db
|
|
53
|
+
.prepare("INSERT OR IGNORE INTO blobs(hash,mime,size,width,height,created_at) VALUES(?,?,?,?,?,?)")
|
|
54
|
+
.run(hash, mime, bytes.byteLength, dims.width ?? null, dims.height ?? null, Date.now());
|
|
55
|
+
const row = this.row(hash);
|
|
56
|
+
return {
|
|
57
|
+
hash,
|
|
58
|
+
mimeType: row?.mime ?? mime,
|
|
59
|
+
bytes: bytes.byteLength,
|
|
60
|
+
...(row?.width != null ? { width: Number(row.width) } : {}),
|
|
61
|
+
...(row?.height != null ? { height: Number(row.height) } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
get(hash) {
|
|
65
|
+
if (!HASH.test(hash))
|
|
66
|
+
return undefined;
|
|
67
|
+
const row = this.row(hash);
|
|
68
|
+
const file = this.path(hash);
|
|
69
|
+
if (!row || !existsSync(file))
|
|
70
|
+
return undefined;
|
|
71
|
+
return { bytes: readFileSync(file), mimeType: row.mime };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolves a reference into a provider-ready image `Attachment` (base64 `data`), verifying
|
|
75
|
+
* that the stored bytes still match the hash.
|
|
76
|
+
*/
|
|
77
|
+
attachment(ref) {
|
|
78
|
+
const found = this.get(ref.hash);
|
|
79
|
+
if (!found)
|
|
80
|
+
throw new Error(`Blob not found: ${ref.hash}`);
|
|
81
|
+
if (createHash("sha256").update(found.bytes).digest("hex") !== ref.hash)
|
|
82
|
+
throw new Error(`Blob content does not match its hash: ${ref.hash}`);
|
|
83
|
+
if (!found.mimeType.startsWith("image/"))
|
|
84
|
+
throw new Error(`Only image blobs can be attached (got ${found.mimeType})`);
|
|
85
|
+
return {
|
|
86
|
+
kind: "image",
|
|
87
|
+
mimeType: found.mimeType,
|
|
88
|
+
data: found.bytes.toString("base64"),
|
|
89
|
+
bytes: found.bytes.byteLength,
|
|
90
|
+
...(ref.width !== undefined ? { width: ref.width } : {}),
|
|
91
|
+
...(ref.height !== undefined ? { height: ref.height } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified line diffs for the `diff` UiBlock that `write_file`/`edit_file` attach for rich
|
|
3
|
+
* clients (spec §8.6, RF-13). Myers O((N+M)·D) on lines after trimming the common prefix and
|
|
4
|
+
* suffix; when the edit distance exceeds `maxD` the middle becomes a full replacement. Pure and
|
|
5
|
+
* portable (no dependency). The web UI ships its own copy of the algorithm for third-party blocks
|
|
6
|
+
* that only carry `before`/`after` (packages cannot share code with the browser bundle).
|
|
7
|
+
*/
|
|
8
|
+
type Op = 0 | 1 | 2;
|
|
9
|
+
/** Line edit script (0 equal, 1 delete, 2 insert). */
|
|
10
|
+
export declare function lineEdits(a: string[], b: string[], maxD?: number): Op[];
|
|
11
|
+
/**
|
|
12
|
+
* A unified patch (`--- a/path`, `+++ b/path`, hunks with `context` lines) from `before` to
|
|
13
|
+
* `after`; `undefined` means the file did not exist. Identical texts give `""`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function unifiedPatch(path: string, before: string | undefined, after: string | undefined, options?: {
|
|
16
|
+
context?: number;
|
|
17
|
+
}): string;
|
|
18
|
+
/** Cuts `text` at a line boundary so it fits in `maxBytes` (UTF-8). */
|
|
19
|
+
export declare function clipLines(text: string, maxBytes: number): {
|
|
20
|
+
text: string;
|
|
21
|
+
clipped: boolean;
|
|
22
|
+
};
|
|
23
|
+
export {};
|