@alisio/core 0.1.0-alpha.12 → 0.1.0-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application.d.ts +2 -0
- package/dist/application.js +12 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +24 -0
- package/dist/core/compaction.js +13 -2
- package/dist/core/contracts.d.ts +2 -0
- package/dist/core/runner.d.ts +7 -0
- package/dist/core/runner.js +18 -3
- package/dist/mcp/connector.js +3 -2
- package/dist/mcp/rich.d.ts +41 -0
- package/dist/mcp/rich.js +290 -0
- package/dist/plugins/host.d.ts +6 -0
- package/dist/plugins/host.js +8 -0
- package/dist/runtime/store.d.ts +2 -0
- package/dist/runtime/store.js +7 -0
- package/dist/tools/execute.js +4 -1
- package/package.json +2 -2
package/dist/application.d.ts
CHANGED
|
@@ -171,6 +171,8 @@ export declare function createApplication(options?: AppOptions): Promise<{
|
|
|
171
171
|
* (next instructions load), `websearch.provider` mutates the shared config object the search
|
|
172
172
|
* chain reads per call, `pluginHooks.timeoutMs` lands in the plugin host (next hook call),
|
|
173
173
|
* and `tui.*` is persisted for the host to apply. Nothing in this set requires a restart.
|
|
174
|
+
* `agents.*` (active agent id and reasoning effort) is persisted for the TUI to consume:
|
|
175
|
+
* the next `/agents`/`/effort` selection and the next prompt already read the live config.
|
|
174
176
|
* `mcp.allow` is deliberately not routed here: it flows through
|
|
175
177
|
* `rememberGlobalMcpConsent`/`revokeGlobalMcpConsent` instead.
|
|
176
178
|
* Throws under `--read-only`. Returns the written config file.
|
package/dist/application.js
CHANGED
|
@@ -743,6 +743,8 @@ export async function createApplication(options = {}) {
|
|
|
743
743
|
* (next instructions load), `websearch.provider` mutates the shared config object the search
|
|
744
744
|
* chain reads per call, `pluginHooks.timeoutMs` lands in the plugin host (next hook call),
|
|
745
745
|
* and `tui.*` is persisted for the host to apply. Nothing in this set requires a restart.
|
|
746
|
+
* `agents.*` (active agent id and reasoning effort) is persisted for the TUI to consume:
|
|
747
|
+
* the next `/agents`/`/effort` selection and the next prompt already read the live config.
|
|
746
748
|
* `mcp.allow` is deliberately not routed here: it flows through
|
|
747
749
|
* `rememberGlobalMcpConsent`/`revokeGlobalMcpConsent` instead.
|
|
748
750
|
* Throws under `--read-only`. Returns the written config file.
|
|
@@ -790,6 +792,16 @@ export async function createApplication(options = {}) {
|
|
|
790
792
|
case "tui.skillSlashCommands":
|
|
791
793
|
config.tui = { ...config.tui, skillSlashCommands: value === true };
|
|
792
794
|
break;
|
|
795
|
+
case "agents.active":
|
|
796
|
+
config.agents = { ...config.agents, active: String(value) };
|
|
797
|
+
break;
|
|
798
|
+
case "agents.effort":
|
|
799
|
+
// Clearing (undefined) drops the leaf from the live config and the JSON file.
|
|
800
|
+
config.agents =
|
|
801
|
+
value === undefined
|
|
802
|
+
? { active: config.agents.active }
|
|
803
|
+
: { ...config.agents, effort: String(value) };
|
|
804
|
+
break;
|
|
793
805
|
case "websearch.provider":
|
|
794
806
|
// Mutated in place (same object identity) so the tool chain's per-call
|
|
795
807
|
// `websearchCtx.config` sees the new provider on the very next search call.
|
package/dist/config.d.ts
CHANGED
|
@@ -177,6 +177,10 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
177
177
|
paddingX: z.ZodDefault<z.ZodNumber>;
|
|
178
178
|
skillSlashCommands: z.ZodDefault<z.ZodBoolean>;
|
|
179
179
|
}, z.core.$strict>>;
|
|
180
|
+
agents: z.ZodDefault<z.ZodObject<{
|
|
181
|
+
active: z.ZodDefault<z.ZodString>;
|
|
182
|
+
effort: z.ZodOptional<z.ZodString>;
|
|
183
|
+
}, z.core.$strict>>;
|
|
180
184
|
}, z.core.$strict>, z.ZodTransform<{
|
|
181
185
|
schemaVersion: 1;
|
|
182
186
|
provider: {
|
|
@@ -232,6 +236,10 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
232
236
|
paddingX: number;
|
|
233
237
|
skillSlashCommands: boolean;
|
|
234
238
|
};
|
|
239
|
+
agents: {
|
|
240
|
+
active: string;
|
|
241
|
+
effort?: string | undefined;
|
|
242
|
+
};
|
|
235
243
|
mcp: {
|
|
236
244
|
allow?: boolean | undefined;
|
|
237
245
|
servers: {
|
|
@@ -370,6 +378,10 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
370
378
|
paddingX: number;
|
|
371
379
|
skillSlashCommands: boolean;
|
|
372
380
|
};
|
|
381
|
+
agents: {
|
|
382
|
+
active: string;
|
|
383
|
+
effort?: string | undefined;
|
|
384
|
+
};
|
|
373
385
|
}>>;
|
|
374
386
|
export type Config = z.infer<typeof configSchema>;
|
|
375
387
|
export type ServerConfig = z.infer<typeof serverSchema>;
|
|
@@ -452,6 +464,8 @@ declare const SETTABLE_KEYS: {
|
|
|
452
464
|
serpapi: "serpapi";
|
|
453
465
|
tavily: "tavily";
|
|
454
466
|
}>>;
|
|
467
|
+
readonly "agents.active": z.ZodDefault<z.ZodString>;
|
|
468
|
+
readonly "agents.effort": z.ZodOptional<z.ZodString>;
|
|
455
469
|
};
|
|
456
470
|
export type SettableSettingKey = keyof typeof SETTABLE_KEYS;
|
|
457
471
|
export declare function isSettableSettingKey(key: string): key is SettableSettingKey;
|
package/dist/config.js
CHANGED
|
@@ -188,6 +188,27 @@ const configObjectSchema = z
|
|
|
188
188
|
})
|
|
189
189
|
.strict()
|
|
190
190
|
.default(() => ({ paddingX: 1, skillSlashCommands: true })),
|
|
191
|
+
/**
|
|
192
|
+
* The ACTIVE agent drives the main session: its system prompt is appended to every prompt and
|
|
193
|
+
* its optional model selector is offered when the agent is chosen (`/agents`). `build` is the
|
|
194
|
+
* built-in default (full-power, no added persona); `plan` is the built-in read-only planner.
|
|
195
|
+
* `effort` holds a reasoning effort level chosen with `/effort` for a model that advertises
|
|
196
|
+
* `effort.supportedLevels`; it is validated against the ACTIVE model on use, falling back to
|
|
197
|
+
* the model's default level when unsupported.
|
|
198
|
+
*/
|
|
199
|
+
agents: z
|
|
200
|
+
.object({
|
|
201
|
+
active: z
|
|
202
|
+
.string()
|
|
203
|
+
.trim()
|
|
204
|
+
.min(1)
|
|
205
|
+
.max(100)
|
|
206
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/, "invalid agent name")
|
|
207
|
+
.default("build"),
|
|
208
|
+
effort: z.string().trim().min(1).max(50).optional(),
|
|
209
|
+
})
|
|
210
|
+
.strict()
|
|
211
|
+
.default(() => ({ active: "build" })),
|
|
191
212
|
})
|
|
192
213
|
.strict()
|
|
193
214
|
.superRefine((config, context) => {
|
|
@@ -434,6 +455,7 @@ const SETTABLE_SECTIONS = {
|
|
|
434
455
|
pluginHooks: configObjectSchema.shape.pluginHooks.removeDefault(),
|
|
435
456
|
tui: configObjectSchema.shape.tui.removeDefault(),
|
|
436
457
|
websearch: configObjectSchema.shape.websearch.removeDefault(),
|
|
458
|
+
agents: configObjectSchema.shape.agents.removeDefault(),
|
|
437
459
|
};
|
|
438
460
|
const SETTABLE_KEYS = {
|
|
439
461
|
"compaction.auto": SETTABLE_SECTIONS.compaction.shape.auto,
|
|
@@ -450,6 +472,8 @@ const SETTABLE_KEYS = {
|
|
|
450
472
|
"tui.paddingX": SETTABLE_SECTIONS.tui.shape.paddingX,
|
|
451
473
|
"tui.skillSlashCommands": SETTABLE_SECTIONS.tui.shape.skillSlashCommands,
|
|
452
474
|
"websearch.provider": SETTABLE_SECTIONS.websearch.shape.provider,
|
|
475
|
+
"agents.active": SETTABLE_SECTIONS.agents.shape.active,
|
|
476
|
+
"agents.effort": SETTABLE_SECTIONS.agents.shape.effort,
|
|
453
477
|
};
|
|
454
478
|
export function isSettableSettingKey(key) {
|
|
455
479
|
return Object.prototype.hasOwnProperty.call(SETTABLE_KEYS, key);
|
package/dist/core/compaction.js
CHANGED
|
@@ -116,6 +116,10 @@ function clampMessage(message, maxToolResultChars, maxTextChars) {
|
|
|
116
116
|
let used = 0;
|
|
117
117
|
let changed = false;
|
|
118
118
|
const content = message.result.content.map((part) => {
|
|
119
|
+
// Only the text projection is clipped: ui/image parts are display-only transcript data and
|
|
120
|
+
// are never truncated (raw bytes never reach the model, which only sees the projection).
|
|
121
|
+
if (part.type !== "text")
|
|
122
|
+
return part;
|
|
119
123
|
const room = maxToolResultChars - used;
|
|
120
124
|
const text = room <= 0
|
|
121
125
|
? TRUNCATION_MARKER
|
|
@@ -213,7 +217,8 @@ export function reduceMessagesToBudget(messages, targetChars, options = {}) {
|
|
|
213
217
|
: m.role === "assistant"
|
|
214
218
|
? m.text.length > textCap
|
|
215
219
|
: m.role === "tool" &&
|
|
216
|
-
m.result.content.reduce((a, p) => a + p.text.length, 0) >
|
|
220
|
+
m.result.content.reduce((a, p) => a + (p.type === "text" ? p.text.length : 0), 0) >
|
|
221
|
+
toolCap;
|
|
217
222
|
if (over) {
|
|
218
223
|
best = i;
|
|
219
224
|
bestSize = current;
|
|
@@ -260,7 +265,12 @@ export function serializeForSummary(messages, maxItemChars = 4_000) {
|
|
|
260
265
|
lines.push(`TOOL CALL ${c.id} ${c.name}: ${clip(c.arguments, maxItemChars)}`);
|
|
261
266
|
}
|
|
262
267
|
else {
|
|
263
|
-
|
|
268
|
+
// Only the text projection is summarized: ui/image parts are display data and never
|
|
269
|
+
// reach the summarizer (raw image bytes in particular stay out of the checkpoint).
|
|
270
|
+
const text = m.result.content
|
|
271
|
+
.filter((p) => p.type === "text")
|
|
272
|
+
.map((c) => c.text)
|
|
273
|
+
.join("\n");
|
|
264
274
|
lines.push(`TOOL RESULT ${m.callId}${m.result.isError ? " (error)" : ""}: ${clip(text, maxItemChars)}`);
|
|
265
275
|
}
|
|
266
276
|
}
|
|
@@ -381,6 +391,7 @@ async function completeTextWithMeta(provider, request) {
|
|
|
381
391
|
signal: request.signal,
|
|
382
392
|
...(request.model ? { model: request.model } : {}),
|
|
383
393
|
...(request.sessionId ? { sessionId: request.sessionId } : {}),
|
|
394
|
+
...(request.reasoningEffort ? { reasoningEffort: request.reasoningEffort } : {}),
|
|
384
395
|
})) {
|
|
385
396
|
request.signal.throwIfAborted();
|
|
386
397
|
if (event.type === "completed") {
|
package/dist/core/contracts.d.ts
CHANGED
|
@@ -66,6 +66,8 @@ export interface SessionStore {
|
|
|
66
66
|
release(id: string): void;
|
|
67
67
|
beginCall(session: string, call: ToolCall): void;
|
|
68
68
|
endCall(session: string, call: ToolCall, result: ToolResult): void;
|
|
69
|
+
/** The persisted result of a completed tool call, for replay of rich (ui/image) parts. */
|
|
70
|
+
callResult(session: string, callId: string): ToolResult | undefined;
|
|
69
71
|
reconcile(id: string, acknowledge?: boolean): void;
|
|
70
72
|
event(id: string, runId: string, type: string, data: unknown): void;
|
|
71
73
|
}
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ export interface RunnerOptions {
|
|
|
42
42
|
* runner; only meaningful for a provider that documents an equivalent server-side tool.
|
|
43
43
|
*/
|
|
44
44
|
nativeTools?: Array<Record<string, unknown>>;
|
|
45
|
+
/**
|
|
46
|
+
* Default reasoning effort level (a value from the current model's `ModelInfo.effort`), sent on
|
|
47
|
+
* every run unless overridden per-run. Providers that do not advertise effort levels ignore it.
|
|
48
|
+
*/
|
|
49
|
+
reasoningEffort?: string;
|
|
45
50
|
}
|
|
46
51
|
/** The settings a running TUI can change live; merged into the runner options on apply. */
|
|
47
52
|
export interface RunnerSettingsPatch {
|
|
@@ -80,6 +85,8 @@ export interface RunOptions {
|
|
|
80
85
|
/** Per-call output token budget for this run; beats the runner-level budget when set. */
|
|
81
86
|
maxOutputTokens?: number;
|
|
82
87
|
timeoutMs?: number;
|
|
88
|
+
/** Reasoning effort level for this run; beats the runner-level default when set. */
|
|
89
|
+
reasoningEffort?: string;
|
|
83
90
|
}
|
|
84
91
|
/**
|
|
85
92
|
* Default cumulative token budget for one run: several context windows (each turn re-sends the
|
package/dist/core/runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { textResult, } from "@alisio/sdk";
|
|
1
|
+
import { 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
|
|
@@ -409,15 +409,22 @@ export class AgentRunner {
|
|
|
409
409
|
let completion;
|
|
410
410
|
let truncated = false;
|
|
411
411
|
let usage;
|
|
412
|
+
// Providers only ever see the text projection of tool results: ui/image parts are a
|
|
413
|
+
// display-only extension of the persisted transcript and never reach a model prompt
|
|
414
|
+
// (raw image bytes included). The store keeps the rich parts for TUI replay.
|
|
415
|
+
const providerMessages = messages.map((m) => m.role === "tool" ? { ...m, result: textProjection(m.result) } : m);
|
|
412
416
|
for await (const e of provider.stream({
|
|
413
417
|
instructions,
|
|
414
|
-
messages,
|
|
418
|
+
messages: providerMessages,
|
|
415
419
|
tools,
|
|
416
420
|
maxOutputTokens: options.maxOutputTokens ?? o.maxOutputTokens ?? 4096,
|
|
417
421
|
signal: combined,
|
|
418
422
|
model,
|
|
419
423
|
sessionId,
|
|
420
424
|
...(o.nativeTools?.length ? { nativeTools: o.nativeTools } : {}),
|
|
425
|
+
...((options.reasoningEffort ?? o.reasoningEffort)
|
|
426
|
+
? { reasoningEffort: options.reasoningEffort ?? o.reasoningEffort }
|
|
427
|
+
: {}),
|
|
421
428
|
})) {
|
|
422
429
|
combined.throwIfAborted();
|
|
423
430
|
if (e.type === "text_delta")
|
|
@@ -495,7 +502,14 @@ export class AgentRunner {
|
|
|
495
502
|
const { call } = p;
|
|
496
503
|
let result;
|
|
497
504
|
const started = Date.now();
|
|
498
|
-
|
|
505
|
+
// `effect` rides along so the TUI can group consecutive calls by capability kind
|
|
506
|
+
// (read batches, write batches, commands…) without a name heuristic. Additive field.
|
|
507
|
+
emit("tool_started", {
|
|
508
|
+
name: call.name,
|
|
509
|
+
id: call.id,
|
|
510
|
+
arguments: call.arguments,
|
|
511
|
+
effect: p.tool?.effect ?? "external",
|
|
512
|
+
});
|
|
499
513
|
try {
|
|
500
514
|
combined.throwIfAborted();
|
|
501
515
|
if (p.error)
|
|
@@ -569,6 +583,7 @@ export class AgentRunner {
|
|
|
569
583
|
isError: result.isError ?? false,
|
|
570
584
|
durationMs: Date.now() - started,
|
|
571
585
|
preview: result.content
|
|
586
|
+
.filter((part) => part.type === "text")
|
|
572
587
|
.map((c) => c.text)
|
|
573
588
|
.join("\n")
|
|
574
589
|
.slice(0, 2_000),
|
package/dist/mcp/connector.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/cli
|
|
|
4
4
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
5
5
|
import { objectSchema } from "../tools/standard.js";
|
|
6
6
|
import { loadVersion } from "../version.js";
|
|
7
|
+
import { mapMcpCallResult, mapMcpResourceResult } from "./rich.js";
|
|
7
8
|
const TOOL_NAME_LIMIT = 64;
|
|
8
9
|
/**
|
|
9
10
|
* Per-server client close cap used ONLY by the terminal `close()`: the MCP SDK's stdio close can
|
|
@@ -111,7 +112,7 @@ export class McpConnector {
|
|
|
111
112
|
const result = i.uri
|
|
112
113
|
? await client.readResource({ uri: String(i.uri) }, { signal: c.signal, timeout: 15000 })
|
|
113
114
|
: await client.listResources(undefined, { signal: c.signal, timeout: 15000 });
|
|
114
|
-
return
|
|
115
|
+
return mapMcpResourceResult(result);
|
|
115
116
|
},
|
|
116
117
|
}));
|
|
117
118
|
undo.push(this.registry.register({
|
|
@@ -329,7 +330,7 @@ export class McpConnector {
|
|
|
329
330
|
JSON.stringify(current.inputSchema) !== JSON.stringify(tool.inputSchema))
|
|
330
331
|
throw new Error("MCP schema changed; reconnect the server to refresh tools");
|
|
331
332
|
const result = await client.callTool({ name: tool.name, arguments: input }, { signal: ctx.signal, timeout: 30000 });
|
|
332
|
-
return
|
|
333
|
+
return mapMcpCallResult(result);
|
|
333
334
|
},
|
|
334
335
|
}));
|
|
335
336
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP → Alisio result mapping ("Nivel 0" rich tool results).
|
|
3
|
+
*
|
|
4
|
+
* MCP protocol types never leak into the SDK: this adapter is the single place that folds an
|
|
5
|
+
* MCP `CallToolResult` / `ReadResourceResult` into the Alisio-owned `ToolResult` content union
|
|
6
|
+
* (text, image, ui blocks). Mapping is heuristic and conservative:
|
|
7
|
+
*
|
|
8
|
+
* - `text` parts stay text parts; an image part becomes `{type:"image"}` (base64 data + mime).
|
|
9
|
+
* - `structuredContent` (or a JSON-parseable text part) is folded into a `ui` block when it
|
|
10
|
+
* matches a verified shape — `{columns,rows}` / `{headers,rows}` tables, all-scalar
|
|
11
|
+
* key-value objects, or `{nodes:[{label,children,meta}]}` trees. Anything else stays text
|
|
12
|
+
* (the previous flattening).
|
|
13
|
+
* - A canonical text projection of every ui block / image is ALWAYS appended as a text part so
|
|
14
|
+
* providers, compaction and headless output only see text.
|
|
15
|
+
* - Unknown call results (no contents, nothing recognized) keep the previous
|
|
16
|
+
* `JSON.stringify(result)` behavior verbatim.
|
|
17
|
+
*/
|
|
18
|
+
import { type ToolResult, type UiBlock } from "@alisio/sdk";
|
|
19
|
+
/**
|
|
20
|
+
* Folds a candidate value into a UiBlock when it matches one of the verified shapes:
|
|
21
|
+
* `{columns,rows}` / `{headers,rows}` tables, `{nodes:[...]}` trees, or flat scalar objects.
|
|
22
|
+
*/
|
|
23
|
+
export declare function blockFromUnknown(value: unknown): UiBlock | undefined;
|
|
24
|
+
/** Tries a JSON-parseable text part: recognized shapes become ui blocks, everything else stays text. */
|
|
25
|
+
export declare function blockFromJsonText(text: string): UiBlock | undefined;
|
|
26
|
+
/** Compact plain-text rendering of a ui block, used as the canonical model-visible projection. */
|
|
27
|
+
export declare function renderUiBlockText(block: UiBlock): string;
|
|
28
|
+
/**
|
|
29
|
+
* Maps an MCP `CallToolResult` into an Alisio `ToolResult`. Text/image parts map directly;
|
|
30
|
+
* verified table/key-value/tree shapes (from `structuredContent` or JSON text parts) become
|
|
31
|
+
* `ui` blocks; a canonical text projection is always appended when non-text parts exist, and
|
|
32
|
+
* unrecognized results keep the previous `JSON.stringify` flattening verbatim.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mapMcpCallResult(result: unknown): ToolResult;
|
|
35
|
+
/**
|
|
36
|
+
* Maps an MCP `ReadResourceResult` / `ListResourcesResult` into an Alisio `ToolResult`.
|
|
37
|
+
* Text contents map to text parts; image blobs become `{type:"image"}` parts; non-image blobs
|
|
38
|
+
* become a text marker (never raw base64 in the model prompt). Results without contents (a
|
|
39
|
+
* resource listing, for example) keep the previous `JSON.stringify` flattening.
|
|
40
|
+
*/
|
|
41
|
+
export declare function mapMcpResourceResult(result: unknown): ToolResult;
|
package/dist/mcp/rich.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP → Alisio result mapping ("Nivel 0" rich tool results).
|
|
3
|
+
*
|
|
4
|
+
* MCP protocol types never leak into the SDK: this adapter is the single place that folds an
|
|
5
|
+
* MCP `CallToolResult` / `ReadResourceResult` into the Alisio-owned `ToolResult` content union
|
|
6
|
+
* (text, image, ui blocks). Mapping is heuristic and conservative:
|
|
7
|
+
*
|
|
8
|
+
* - `text` parts stay text parts; an image part becomes `{type:"image"}` (base64 data + mime).
|
|
9
|
+
* - `structuredContent` (or a JSON-parseable text part) is folded into a `ui` block when it
|
|
10
|
+
* matches a verified shape — `{columns,rows}` / `{headers,rows}` tables, all-scalar
|
|
11
|
+
* key-value objects, or `{nodes:[{label,children,meta}]}` trees. Anything else stays text
|
|
12
|
+
* (the previous flattening).
|
|
13
|
+
* - A canonical text projection of every ui block / image is ALWAYS appended as a text part so
|
|
14
|
+
* providers, compaction and headless output only see text.
|
|
15
|
+
* - Unknown call results (no contents, nothing recognized) keep the previous
|
|
16
|
+
* `JSON.stringify(result)` behavior verbatim.
|
|
17
|
+
*/
|
|
18
|
+
import { textResult } from "@alisio/sdk";
|
|
19
|
+
const scalar = (value) => typeof value === "string"
|
|
20
|
+
? value
|
|
21
|
+
: typeof value === "number" || typeof value === "boolean" || value === null
|
|
22
|
+
? String(value)
|
|
23
|
+
: undefined;
|
|
24
|
+
/** Cell rendering: scalars verbatim, nested values compact-JSON'd (never raw bytes). */
|
|
25
|
+
const cellText = (value) => typeof value === "string"
|
|
26
|
+
? value
|
|
27
|
+
: value === null || value === undefined
|
|
28
|
+
? ""
|
|
29
|
+
: typeof value === "object"
|
|
30
|
+
? JSON.stringify(value)
|
|
31
|
+
: String(value);
|
|
32
|
+
function tableShape(value) {
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
34
|
+
return undefined;
|
|
35
|
+
const record = value;
|
|
36
|
+
const columnsRaw = Array.isArray(record.columns)
|
|
37
|
+
? record.columns
|
|
38
|
+
: Array.isArray(record.headers)
|
|
39
|
+
? record.headers
|
|
40
|
+
: undefined;
|
|
41
|
+
const rowsRaw = Array.isArray(record.rows) ? record.rows : undefined;
|
|
42
|
+
if (!columnsRaw || !rowsRaw)
|
|
43
|
+
return undefined;
|
|
44
|
+
const columns = [];
|
|
45
|
+
for (const column of columnsRaw) {
|
|
46
|
+
const text = scalar(column);
|
|
47
|
+
if (text === undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
columns.push(text);
|
|
50
|
+
}
|
|
51
|
+
const rows = [];
|
|
52
|
+
for (const row of rowsRaw) {
|
|
53
|
+
if (!row || typeof row !== "object")
|
|
54
|
+
return undefined;
|
|
55
|
+
if (Array.isArray(row)) {
|
|
56
|
+
rows.push(row.map(cellText));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Object rows are keyed by the declared columns (`{columns, rows:[{a:1,b:2}]}`).
|
|
60
|
+
const named = row;
|
|
61
|
+
rows.push(columns.map((column) => cellText(named[column])));
|
|
62
|
+
}
|
|
63
|
+
if (!columns.length || !rows.length)
|
|
64
|
+
return undefined;
|
|
65
|
+
return { kind: "table", columns, rows };
|
|
66
|
+
}
|
|
67
|
+
function treeNode(value) {
|
|
68
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
69
|
+
return undefined;
|
|
70
|
+
const record = value;
|
|
71
|
+
const label = scalar(record.label) ?? scalar(record.name);
|
|
72
|
+
if (label === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
const meta = scalar(record.meta) ?? scalar(record.value) ?? scalar(record.detail);
|
|
75
|
+
const node = { label };
|
|
76
|
+
if (meta !== undefined && meta !== label)
|
|
77
|
+
node.meta = meta;
|
|
78
|
+
const children = Array.isArray(record.children) ? record.children : undefined;
|
|
79
|
+
if (children) {
|
|
80
|
+
const mapped = [];
|
|
81
|
+
for (const child of children) {
|
|
82
|
+
const next = treeNode(child);
|
|
83
|
+
if (!next)
|
|
84
|
+
return undefined;
|
|
85
|
+
mapped.push(next);
|
|
86
|
+
}
|
|
87
|
+
node.children = mapped;
|
|
88
|
+
}
|
|
89
|
+
return node;
|
|
90
|
+
}
|
|
91
|
+
function treeShape(value) {
|
|
92
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
93
|
+
return undefined;
|
|
94
|
+
const record = value;
|
|
95
|
+
const nodes = Array.isArray(record.nodes)
|
|
96
|
+
? record.nodes
|
|
97
|
+
: record.tree !== undefined && typeof record.tree === "object" && !Array.isArray(record.tree)
|
|
98
|
+
? [record.tree]
|
|
99
|
+
: undefined;
|
|
100
|
+
if (!nodes)
|
|
101
|
+
return undefined;
|
|
102
|
+
const mapped = [];
|
|
103
|
+
for (const node of nodes) {
|
|
104
|
+
const next = treeNode(node);
|
|
105
|
+
if (!next)
|
|
106
|
+
return undefined;
|
|
107
|
+
mapped.push(next);
|
|
108
|
+
}
|
|
109
|
+
return mapped.length ? { kind: "tree", nodes: mapped } : undefined;
|
|
110
|
+
}
|
|
111
|
+
function keyValueShape(value) {
|
|
112
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
113
|
+
return undefined;
|
|
114
|
+
const record = value;
|
|
115
|
+
const entries = Object.entries(record);
|
|
116
|
+
if (!entries.length || entries.some(([, v]) => typeof v === "object" && v !== null))
|
|
117
|
+
return undefined;
|
|
118
|
+
return { kind: "key-value", entries: entries.map(([key, v]) => [key, String(v)]) };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Folds a candidate value into a UiBlock when it matches one of the verified shapes:
|
|
122
|
+
* `{columns,rows}` / `{headers,rows}` tables, `{nodes:[...]}` trees, or flat scalar objects.
|
|
123
|
+
*/
|
|
124
|
+
export function blockFromUnknown(value) {
|
|
125
|
+
return tableShape(value) ?? treeShape(value) ?? keyValueShape(value);
|
|
126
|
+
}
|
|
127
|
+
/** Tries a JSON-parseable text part: recognized shapes become ui blocks, everything else stays text. */
|
|
128
|
+
export function blockFromJsonText(text) {
|
|
129
|
+
const trimmed = text.trim();
|
|
130
|
+
if (!trimmed.startsWith("{"))
|
|
131
|
+
return undefined;
|
|
132
|
+
try {
|
|
133
|
+
return blockFromUnknown(JSON.parse(trimmed));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Compact plain-text rendering of a ui block, used as the canonical model-visible projection. */
|
|
140
|
+
export function renderUiBlockText(block) {
|
|
141
|
+
switch (block.kind) {
|
|
142
|
+
case "table": {
|
|
143
|
+
const head = `| ${block.columns.join(" | ")} |`;
|
|
144
|
+
const sep = `| ${block.columns.map(() => "---").join(" | ")} |`;
|
|
145
|
+
const rows = block.rows.map((row) => `| ${row.join(" | ")} |`);
|
|
146
|
+
return [block.caption ? `[table: ${block.caption}]` : "", head, sep, ...rows]
|
|
147
|
+
.filter((line) => line !== "")
|
|
148
|
+
.join("\n");
|
|
149
|
+
}
|
|
150
|
+
case "key-value":
|
|
151
|
+
return [
|
|
152
|
+
block.caption ? `[key-value: ${block.caption}]` : "",
|
|
153
|
+
...block.entries.map(([key, value]) => `${key}: ${value}`),
|
|
154
|
+
]
|
|
155
|
+
.filter((line) => line !== "")
|
|
156
|
+
.join("\n");
|
|
157
|
+
case "tree": {
|
|
158
|
+
const lines = [];
|
|
159
|
+
const walk = (nodes, prefix) => {
|
|
160
|
+
nodes.forEach((node, index) => {
|
|
161
|
+
const last = index === nodes.length - 1;
|
|
162
|
+
lines.push(`${prefix}${last ? "└─ " : "├─ "}${node.label}${node.meta ? ` (${node.meta})` : ""}`);
|
|
163
|
+
if (node.children?.length)
|
|
164
|
+
walk(node.children, `${prefix}${last ? " " : "│ "}`);
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
walk(block.nodes, "");
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|
|
170
|
+
case "code": {
|
|
171
|
+
const fence = "```";
|
|
172
|
+
const lang = block.lang ? `${block.lang}\n` : "\n";
|
|
173
|
+
const body = block.code.endsWith("\n") ? block.code : `${block.code}\n`;
|
|
174
|
+
return [block.caption ? `[code: ${block.caption}]` : "", `${fence}${lang}${body}${fence}`]
|
|
175
|
+
.filter((line) => line !== "")
|
|
176
|
+
.join("\n");
|
|
177
|
+
}
|
|
178
|
+
case "markdown":
|
|
179
|
+
return block.text;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const base64Bytes = (data) => Math.floor((data.length * 3) / 4);
|
|
183
|
+
const projectionText = (parts) => parts
|
|
184
|
+
.map((part) => part.type === "ui"
|
|
185
|
+
? renderUiBlockText(part.block)
|
|
186
|
+
: `[image: ${part.mimeType} (${base64Bytes(part.data)} bytes)]`)
|
|
187
|
+
.join("\n");
|
|
188
|
+
/**
|
|
189
|
+
* Maps an MCP `CallToolResult` into an Alisio `ToolResult`. Text/image parts map directly;
|
|
190
|
+
* verified table/key-value/tree shapes (from `structuredContent` or JSON text parts) become
|
|
191
|
+
* `ui` blocks; a canonical text projection is always appended when non-text parts exist, and
|
|
192
|
+
* unrecognized results keep the previous `JSON.stringify` flattening verbatim.
|
|
193
|
+
*/
|
|
194
|
+
export function mapMcpCallResult(result) {
|
|
195
|
+
const raw = (result ?? {});
|
|
196
|
+
const parts = Array.isArray(raw.content) ? raw.content : [];
|
|
197
|
+
if (!parts.length && raw.structuredContent === undefined)
|
|
198
|
+
return textResult(JSON.stringify(result), raw.isError === true);
|
|
199
|
+
const content = [];
|
|
200
|
+
const rich = [];
|
|
201
|
+
const keepSourceOrder = (part) => {
|
|
202
|
+
rich.push(part);
|
|
203
|
+
content.push(part);
|
|
204
|
+
};
|
|
205
|
+
for (const part of parts) {
|
|
206
|
+
if (!part || typeof part !== "object")
|
|
207
|
+
continue;
|
|
208
|
+
const record = part;
|
|
209
|
+
const type = record.type;
|
|
210
|
+
if (type === "text") {
|
|
211
|
+
const text = typeof record.text === "string" ? record.text : JSON.stringify(record.text ?? "");
|
|
212
|
+
const block = blockFromJsonText(text);
|
|
213
|
+
if (block)
|
|
214
|
+
keepSourceOrder({ type: "ui", block });
|
|
215
|
+
else
|
|
216
|
+
content.push({ type: "text", text });
|
|
217
|
+
}
|
|
218
|
+
else if (type === "image") {
|
|
219
|
+
const data = typeof record.data === "string" ? record.data : "";
|
|
220
|
+
if (data)
|
|
221
|
+
keepSourceOrder({
|
|
222
|
+
type: "image",
|
|
223
|
+
mimeType: typeof record.mimeType === "string" ? record.mimeType : "application/octet-stream",
|
|
224
|
+
data,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// Audio / embedded resource / unknown part: never forward raw bytes; a marker keeps the model informed.
|
|
229
|
+
content.push({ type: "text", text: `[mcp part: ${type}]` });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const structured = blockFromUnknown(raw.structuredContent);
|
|
233
|
+
if (structured)
|
|
234
|
+
keepSourceOrder({ type: "ui", block: structured });
|
|
235
|
+
if (rich.length)
|
|
236
|
+
content.push({ type: "text", text: projectionText(rich) });
|
|
237
|
+
if (!content.length)
|
|
238
|
+
return textResult(JSON.stringify(result), raw.isError === true);
|
|
239
|
+
return { content, ...(raw.isError === true ? { isError: true } : {}) };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Maps an MCP `ReadResourceResult` / `ListResourcesResult` into an Alisio `ToolResult`.
|
|
243
|
+
* Text contents map to text parts; image blobs become `{type:"image"}` parts; non-image blobs
|
|
244
|
+
* become a text marker (never raw base64 in the model prompt). Results without contents (a
|
|
245
|
+
* resource listing, for example) keep the previous `JSON.stringify` flattening.
|
|
246
|
+
*/
|
|
247
|
+
export function mapMcpResourceResult(result) {
|
|
248
|
+
const raw = (result ?? {});
|
|
249
|
+
const contents = Array.isArray(raw.contents) ? raw.contents : [];
|
|
250
|
+
if (!contents.length)
|
|
251
|
+
return textResult(JSON.stringify(result), typeof result === "object" &&
|
|
252
|
+
result !== null &&
|
|
253
|
+
result.isError === true);
|
|
254
|
+
const content = [];
|
|
255
|
+
const rich = [];
|
|
256
|
+
for (const item of contents) {
|
|
257
|
+
if (!item || typeof item !== "object")
|
|
258
|
+
continue;
|
|
259
|
+
const record = item;
|
|
260
|
+
const mimeType = typeof record.mimeType === "string" ? record.mimeType : "application/octet-stream";
|
|
261
|
+
if (typeof record.text === "string") {
|
|
262
|
+
const block = blockFromJsonText(record.text);
|
|
263
|
+
if (block) {
|
|
264
|
+
rich.push({ type: "ui", block });
|
|
265
|
+
content.push({ type: "ui", block });
|
|
266
|
+
}
|
|
267
|
+
else
|
|
268
|
+
content.push({ type: "text", text: record.text });
|
|
269
|
+
}
|
|
270
|
+
else if (typeof record.blob === "string") {
|
|
271
|
+
if (mimeType.startsWith("image/")) {
|
|
272
|
+
const image = { type: "image", mimeType, data: record.blob };
|
|
273
|
+
rich.push(image);
|
|
274
|
+
content.push(image);
|
|
275
|
+
}
|
|
276
|
+
else
|
|
277
|
+
content.push({
|
|
278
|
+
type: "text",
|
|
279
|
+
text: `[resource blob: ${mimeType} (${base64Bytes(record.blob)} bytes)]`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (rich.length)
|
|
284
|
+
content.push({ type: "text", text: projectionText(rich) });
|
|
285
|
+
if (!content.length)
|
|
286
|
+
return textResult(JSON.stringify(result), typeof result === "object" &&
|
|
287
|
+
result !== null &&
|
|
288
|
+
result.isError === true);
|
|
289
|
+
return { content };
|
|
290
|
+
}
|
package/dist/plugins/host.d.ts
CHANGED
|
@@ -72,6 +72,12 @@ export declare class PluginHost implements RunnerExtensions {
|
|
|
72
72
|
askQuestions(request: AskQuestionsRequest): Promise<AskQuestionsResult>;
|
|
73
73
|
open(sessionId: string): boolean;
|
|
74
74
|
}): void;
|
|
75
|
+
/**
|
|
76
|
+
* Read-only view of one plugin's shared state (written through `api.state.set`). The host layer
|
|
77
|
+
* uses this to consume plugin-published data (for example the main-capable agent definitions the
|
|
78
|
+
* built-in subagents plugin publishes) without importing the plugin package.
|
|
79
|
+
*/
|
|
80
|
+
pluginState(plugin: string, key: string): unknown;
|
|
75
81
|
get sessions(): PluginAPI["sessions"];
|
|
76
82
|
/**
|
|
77
83
|
* Live accessor for core standard tools (e.g. `ask_user_question`), which are not "a plugin"
|
package/dist/plugins/host.js
CHANGED
|
@@ -60,6 +60,14 @@ export class PluginHost {
|
|
|
60
60
|
setInteractiveUI(ui) {
|
|
61
61
|
this.uiImpl = ui;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Read-only view of one plugin's shared state (written through `api.state.set`). The host layer
|
|
65
|
+
* uses this to consume plugin-published data (for example the main-capable agent definitions the
|
|
66
|
+
* built-in subagents plugin publishes) without importing the plugin package.
|
|
67
|
+
*/
|
|
68
|
+
pluginState(plugin, key) {
|
|
69
|
+
return this.state.getState(plugin, key);
|
|
70
|
+
}
|
|
63
71
|
get sessions() {
|
|
64
72
|
if (!this.sessionsImpl)
|
|
65
73
|
throw new Error("Child sessions are not available yet");
|
package/dist/runtime/store.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare class SQLiteStore implements SessionStore {
|
|
|
30
30
|
release(id: string): void;
|
|
31
31
|
beginCall(id: string, call: ToolCall): void;
|
|
32
32
|
endCall(id: string, call: ToolCall, result: ToolResult): void;
|
|
33
|
+
/** Rich (ui/image) parts persist as part of the serialized result and replay verbatim. */
|
|
34
|
+
callResult(id: string, callId: string): ToolResult | undefined;
|
|
33
35
|
reconcile(id: string, acknowledge?: boolean): void;
|
|
34
36
|
event(id: string, runId: string, type: string, data: unknown): void;
|
|
35
37
|
getState(plugin: string, key: string): unknown;
|
package/dist/runtime/store.js
CHANGED
|
@@ -211,6 +211,13 @@ export class SQLiteStore {
|
|
|
211
211
|
.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")
|
|
212
212
|
.run(id, call.id, JSON.stringify(result));
|
|
213
213
|
}
|
|
214
|
+
/** Rich (ui/image) parts persist as part of the serialized result and replay verbatim. */
|
|
215
|
+
callResult(id, callId) {
|
|
216
|
+
const row = this.db
|
|
217
|
+
.prepare("SELECT result FROM tool_calls WHERE session=? AND call_id=? AND status='completed'")
|
|
218
|
+
.get(id, callId);
|
|
219
|
+
return row?.result ? JSON.parse(row.result) : undefined;
|
|
220
|
+
}
|
|
214
221
|
reconcile(id, acknowledge = false) {
|
|
215
222
|
this.db.transaction(() => {
|
|
216
223
|
const messages = this.messages(id);
|
package/dist/tools/execute.js
CHANGED
|
@@ -54,7 +54,10 @@ export async function runExecute(code, deps) {
|
|
|
54
54
|
...(deps.session ? { session: deps.session } : {}),
|
|
55
55
|
...(deps.label ? { label: deps.label } : {}),
|
|
56
56
|
});
|
|
57
|
-
const text = result.content
|
|
57
|
+
const text = result.content
|
|
58
|
+
.filter((p) => p.type === "text")
|
|
59
|
+
.map((c) => c.text)
|
|
60
|
+
.join("\n");
|
|
58
61
|
if (result.isError)
|
|
59
62
|
throw new Error(text);
|
|
60
63
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.14",
|
|
4
4
|
"description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=22.16"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@alisio/sdk": "0.1.0-alpha.
|
|
44
|
+
"@alisio/sdk": "0.1.0-alpha.9",
|
|
45
45
|
"@mixmark-io/domino": "2.2.0",
|
|
46
46
|
"@modelcontextprotocol/client": "2.1.0",
|
|
47
47
|
"ajv": "8.20.0",
|