@alisio/alisio-code 0.5.2 → 0.7.0

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.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `alisio forge status`: what `GET /api/forge` will report, for the terminal. It builds the engine
3
+ * from the configuration alone (no application, no database): the WASM is checked against its
4
+ * pinned hash, the host files are looked for, and `--probe` runs a real isolate end to end.
5
+ */
6
+ type Common = {
7
+ cwd?: string;
8
+ config?: string;
9
+ trustProject?: boolean;
10
+ json?: boolean;
11
+ };
12
+ export declare function printForgeStatus(o: Common, probe: boolean): Promise<void>;
13
+ export {};
package/dist/forge.js ADDED
@@ -0,0 +1,46 @@
1
+ const mib = (bytes) => `${Math.round(bytes / (1024 * 1024))} MiB`;
2
+ export async function printForgeStatus(o, probe) {
3
+ const core = await import("@alisio/core");
4
+ const workspace = await core.findWorkspace(o.cwd ?? process.cwd());
5
+ const { config } = await core.loadConfigWithProvenance(workspace, {
6
+ file: o.config,
7
+ trustProject: o.trustProject,
8
+ });
9
+ const engine = core.forgeEngineFor(config.forge);
10
+ const { pythonBackendDescriptors } = await import("./analysis.js");
11
+ const python = await pythonBackendDescriptors(o);
12
+ const base = await core.describeForge(config.forge, engine);
13
+ const status = {
14
+ ...base,
15
+ engines: [...base.engines, ...python],
16
+ python: { runtime: config.analysis.runtime, isolation: config.analysis.isolation },
17
+ };
18
+ const probed = probe ? await engine.probe() : undefined;
19
+ if (o.json) {
20
+ process.stdout.write(`${JSON.stringify({ ...status, ...(probed ? { probe: probed } : {}) })}\n`);
21
+ return;
22
+ }
23
+ const js = status.javascript;
24
+ const descriptor = status.engines[0];
25
+ const lines = [
26
+ `JavaScript ${js.enabled ? "enabled" : "disabled"} ${descriptor?.ready ? "ready" : "not ready"}`,
27
+ ` engine ${descriptor?.id ?? "javascript-quickjs"} ${js.version}`,
28
+ ` isolation ${descriptor?.isolation ?? "none"} (${js.host} host${js.hostSetting === "auto" ? "" : `, forced by forge.javascript.host=${js.hostSetting}`})`,
29
+ ` wasm sha256 ${js.wasmSha256}${js.verified ? " verified" : ""}`,
30
+ ];
31
+ if (!descriptor?.ready && descriptor?.reason)
32
+ lines.push(` reason ${descriptor.reason}`);
33
+ if (js.verification)
34
+ lines.push(` problem ${js.verification}`);
35
+ lines.push(` limits timeout ${js.limits.timeoutMs} ms, heap ${mib(js.limits.memoryBytes)}, stack ${Math.round(js.limits.stackBytes / 1024)} KiB, ${js.limits.maxToolCalls} calls (${js.limits.maxConcurrency} at once), ${js.limits.maxOutputChars} output chars`, ` state ${status.state.enabled ? "on" : "off"} scratchpad ${status.scratch.enabled ? "on" : "off"}`);
36
+ lines.push("", `Python analysis.runtime=${config.analysis.runtime} isolation=${config.analysis.isolation}`);
37
+ for (const backend of python)
38
+ lines.push(` ${backend.id.replace(/^python-/, "").padEnd(10)} ${backend.isolation.padEnd(13)} ${backend.ready ? "ready " : "not ready"} network ${backend.capabilities.networkDeniedByOS ? "denied by the OS" : "not blocked"}${backend.profiles?.length ? ` profiles ${backend.profiles.join(",")}` : ""}${backend.reason ? ` (${backend.reason})` : ""}`);
39
+ if (probed)
40
+ lines.push(probed.ok
41
+ ? ` probe ok, a ${probed.host} isolate ran a script in ${probed.bootMs} ms`
42
+ : ` probe FAILED (${probed.host} host): ${probed.reason}`);
43
+ process.stdout.write(`${lines.join("\n")}\n`);
44
+ if (probed && !probed.ok)
45
+ process.exitCode = 1;
46
+ }
package/dist/main.js CHANGED
@@ -419,6 +419,8 @@ program.command("doctor").action(async (_opts, cmd) => {
419
419
  python: { mode: analysisInfo.status.mode, ...pythonInfo },
420
420
  analysis: {
421
421
  mode: analysisInfo.status.mode,
422
+ ...(analysisInfo.status.isolation ? { isolation: analysisInfo.status.isolation } : {}),
423
+ ...(analysisInfo.status.backends ? { backends: analysisInfo.status.backends } : {}),
422
424
  oci: analysisInfo.status.oci,
423
425
  limits: analysisInfo.status.limits,
424
426
  retention: analysisInfo.status.retention,
@@ -447,13 +449,21 @@ analysis
447
449
  });
448
450
  analysis
449
451
  .command("setup")
450
- .description("Optional: install hash-locked Python extras (--extras) or pull the container image (--oci). " +
451
- "python_run never needs this for the standard library.")
452
+ .description("Optional: install hash-locked Python extras (--extras), pull the container image (--oci) or " +
453
+ "download the WebAssembly runtime (--wasm). python_run never needs this for the standard library.")
452
454
  .option("--extras <set>", "analysis (pandas, numpy, matplotlib…) or science (adds scipy…)")
453
455
  .option("--oci", "Pull the container image once and verify its digest (Docker or Podman)")
454
456
  .option("--image <name>", "With --oci: the image to pull (default: analysis.oci.image)")
457
+ .option("--wasm", "Download the pinned Pyodide runtime for the wasm backend (needs network)")
458
+ .option("--profile <name>", "With --wasm: analysis (default) or science (adds scipy, scikit-learn…)")
459
+ .option("--yes", "With --wasm: do not ask for confirmation (required when not interactive)")
455
460
  .action(async (flags, cmd) => {
456
- const { setupExtras, setupOci } = await import("./analysis.js");
461
+ const { setupExtras, setupOci, setupWasm } = await import("./analysis.js");
462
+ if (flags.wasm)
463
+ return setupWasm(options(cmd), {
464
+ ...(flags.profile ? { profile: flags.profile } : {}),
465
+ yes: !!flags.yes,
466
+ });
457
467
  if (flags.oci)
458
468
  return setupOci(options(cmd), flags.image);
459
469
  return setupExtras(options(cmd), flags.extras);
@@ -466,6 +476,79 @@ analysis
466
476
  const { sweep } = await import("./analysis.js");
467
477
  await sweep(options(cmd), !!flags.force);
468
478
  });
479
+ const forge = program
480
+ .command("forge")
481
+ .description("Forja: isolated code execution (js_run). Shows the engine and why it is or is not ready");
482
+ forge
483
+ .command("status")
484
+ .description("Show the engines: JavaScript (version, WASM hash, isolation, host, limits) and the Python backends")
485
+ .option("--probe", "Also start a real isolate and run a trivial script through the protocol")
486
+ .action(async (flags, cmd) => {
487
+ const { printForgeStatus } = await import("./forge.js");
488
+ await printForgeStatus(options(cmd), !!flags.probe);
489
+ });
490
+ const evalCommand = program
491
+ .command("eval")
492
+ .description("Reliability lab: run a task suite k times, report pass rates with Wilson intervals, compare reports or config variants, record cassettes and replay them without a model");
493
+ /** `alisio eval` entry points: lazy, and exit codes are returned, not thrown (0 ok, 1 error, 2 replay diverged, 3 incompatible). */
494
+ async function runEval(cmd, action) {
495
+ const api = await import("./eval.js");
496
+ const env = await api.realEvalEnv(async () => options(cmd), async () => cliDefaults());
497
+ process.exitCode = await action(api, env, cmd.optsWithGlobals());
498
+ }
499
+ const evalRunOptions = (command) => command
500
+ .option("--tasks <ids>", "Comma-separated task ids (default: every task of the suite)")
501
+ .option("--k <n>", "Repetitions per task (default 5; below 5 the report warns of low power)")
502
+ .option("--seed <n>", "Seed of the run order; each run gets seed + repetition")
503
+ .option("--mode <mode>", "live (default) or replay (serve the recorded cassettes: no provider, no network; measures the harness, not the model)")
504
+ .option("--cassette-dir <dir>", "Cassette directory (default: <suite>/cassettes; used by --mode replay and by record)")
505
+ .option("--keep", "Keep each run's temporary directory (it prints the paths)")
506
+ .option("--out <dir>", "Write the report files here (default: <state>/eval/<runId>/)")
507
+ .option("--suite-dir <dir>", "A suite, or a directory of suites; third-party code: needs --yes")
508
+ .option("--allow-suite-full", "Let a suite run tasks that ask for the full permission mode")
509
+ .option("--yes", "Confirm running a --suite-dir suite without asking (required if not interactive)")
510
+ .option("--input-rate <usd>", "USD per million input tokens, to estimate cost (needs --output-rate)")
511
+ .option("--output-rate <usd>", "USD per million output tokens (needs --input-rate)");
512
+ evalCommand
513
+ .command("list")
514
+ .description("List the available suites (bundled ones plus --suite-dir)")
515
+ .option("--suite-dir <dir>", "A suite, or a directory of suites")
516
+ .action((_flags, cmd) => runEval(cmd, (api, env, flags) => api.evalList(env, flags)));
517
+ evalRunOptions(evalCommand
518
+ .command("run")
519
+ .description("Run a suite k times (live, or --mode replay from cassettes) and write report.json, report.md and runs.jsonl; --json prints the run records as JSONL and the report last. Exit 0 when it completed (whatever passed), 1 on a usage or infrastructure error, 2 when a replay diverged from its cassette")
520
+ .argument("<suite>")).action((suite, _flags, cmd) => runEval(cmd, (api, env, flags) => api.evalRun(env, flags, suite)));
521
+ evalCommand
522
+ .command("record")
523
+ .description("Run each task of a suite ONCE live and write <suite>/cassettes/<taskId>.cassette.jsonl for `eval run --mode replay`. Refuses (exit 1) a cassette that holds a secret or is over 256 KB (2 MB per directory)")
524
+ .argument("<suite>")
525
+ .option("--tasks <ids>", "Comma-separated task ids (default: every task of the suite)")
526
+ .option("--seed <n>", "Seed of the run order")
527
+ .option("--keep", "Keep each run's temporary directory (it prints the paths)")
528
+ .option("--cassette-dir <dir>", "Write the cassettes here (default: <suite>/cassettes)")
529
+ .option("--suite-dir <dir>", "A suite, or a directory of suites; third-party code: needs --yes")
530
+ .option("--allow-suite-full", "Let a suite run tasks that ask for the full permission mode")
531
+ .option("--yes", "Confirm running a --suite-dir suite without asking (required if not interactive)")
532
+ .action((suite, _flags, cmd) => runEval(cmd, (api, env, flags) => api.evalRecord(env, flags, suite)));
533
+ evalCommand
534
+ .command("compare")
535
+ .description("Compare two reports of the same suite and version; exit 3 when the suites are incompatible")
536
+ .argument("<reportA>", "Baseline report.json (or the directory that holds it)")
537
+ .argument("<reportB>", "Candidate report.json (or the directory that holds it)")
538
+ .option("--md", "Print Markdown (the default; --json prints JSON instead)")
539
+ .action((a, b, _flags, cmd) => runEval(cmd, (api, env, flags) => api.evalCompare(env, flags, a, b)));
540
+ evalRunOptions(evalCommand
541
+ .command("variants")
542
+ .description('Run two or more config variants over the same tasks and seeds and compare them with the first. compare.json: {"suite":"id","variants":[{"id":"baseline","overrides":{}},{"id":"early","overrides":{"compaction.threshold":0.6}}]}')
543
+ .argument("<suite>")
544
+ .requiredOption("--variants <file>", "The compare.json file")).action((suite, _flags, cmd) => runEval(cmd, (api, env, flags) => api.evalVariants(env, flags, suite)));
545
+ evalCommand
546
+ .command("export")
547
+ .description("Write ONE shareable JSON bundle ({schemaVersion, report, runs, markdown}) of a stored run (<state>/eval/<runId>/) with absolute paths replaced by placeholders and endpoint hosts removed from provider ids. Refuses (exit 1) when a secret is found, naming the field; refuses to overwrite FILE without --force. Local only: nothing is uploaded. Scrubbing is pattern-based, not a guarantee")
548
+ .argument("<runId>", "Directory name of the run under <state>/eval/")
549
+ .requiredOption("--out <file>", "The bundle to write")
550
+ .option("--force", "Overwrite FILE if it exists")
551
+ .action((runId, _flags, cmd) => runEval(cmd, (api, env, flags) => api.evalExport(env, flags, runId)));
469
552
  const trust = program.command("trust").description("Inspect or revoke per-directory project trust");
470
553
  trust.command("list").action(async () => {
471
554
  const { listTrust } = await import("@alisio/core");
@@ -665,7 +748,14 @@ program
665
748
  await cliUpdateAll(common);
666
749
  });
667
750
  try {
668
- await program.parseAsync();
751
+ // The compiled binary starts its own Forja process isolates with this hidden command (spec D-21).
752
+ // It is dispatched before any configuration or database is loaded and is not part of the CLI.
753
+ if (process.argv.slice(2)[0] === "__forge-isolate") {
754
+ const { runProcessIsolate } = await import("@alisio/core/forge-isolate");
755
+ await runProcessIsolate();
756
+ }
757
+ else
758
+ await program.parseAsync();
669
759
  }
670
760
  catch (error) {
671
761
  console.error(error instanceof Error ? error.message : String(error));
@@ -51,4 +51,4 @@ export declare const installApprovalTitle: (install: InstallPreview) => string;
51
51
  /** Body of the `analysis.install` approval: the packages, the download estimate and the network. */
52
52
  export declare function installApprovalDetail(install: InstallPreview): string;
53
53
  /** Title of a capability approval in the TUI picker. */
54
- export declare const capabilityApprovalTitle: (runtime: "managed" | "oci" | undefined) => string;
54
+ export declare const capabilityApprovalTitle: (runtime: "auto" | "managed" | "oci" | "wasm" | undefined) => string;
@@ -204,4 +204,8 @@ export function installApprovalDetail(install) {
204
204
  /** Title of a capability approval in the TUI picker. */
205
205
  export const capabilityApprovalTitle = (runtime) => runtime === "oci"
206
206
  ? "Run Python analysis (container · no network)?"
207
- : "Run Python analysis (managed · not sandboxed)?";
207
+ : runtime === "wasm"
208
+ ? "Run Python analysis (wasm · restricted process, network not blocked)?"
209
+ : runtime === "auto"
210
+ ? "Run Python analysis (most isolated backend ready; the host only if none · see the engine line)?"
211
+ : "Run Python analysis (managed · not sandboxed)?";
@@ -8,7 +8,7 @@ import { branchDisplay } from "./git-branch.js";
8
8
  import { initialPlanReviewState, reducePlanReview, } from "./plan-review.js";
9
9
  import { initialQuestionState, reduceQuestions, } from "./questions.js";
10
10
  import { scrollWindow } from "./scroll.js";
11
- import { contextLevel, contextPercent, editSummary, entryKeyOf, fitIdentityParts, fitSegments, foldCandidateOf, formatContext, formatDuration, formatTokens, groupIdentity, groupToolEntries, humanizeToolName, isUiBlock, previewLinesFor, previewRows, quietFor, REASONING_EXPAND_MAX_LINES, reasoningDurationMs, runPhase, STALLED_MS, } from "./state.js";
11
+ import { contextLevel, contextPercent, editSummary, entryKeyOf, fitIdentityParts, fitSegments, foldCandidateOf, forgeLabel, formatContext, formatDuration, formatTokens, groupIdentity, groupToolEntries, humanizeToolName, isUiBlock, previewLinesFor, previewRows, quietFor, REASONING_EXPAND_MAX_LINES, reasoningDurationMs, runPhase, STALLED_MS, } from "./state.js";
12
12
  import { highlightCode, imageTheme, levelColor, markdownDefaultTextStyle, markdownTheme, markdownTransform, sanitizeCode, style, } from "./theme.js";
13
13
  export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
14
14
  /** Shared animation clock advanced by the app while work is running. */
@@ -643,7 +643,9 @@ export class ToolBlock {
643
643
  ? style.gray(formatDuration(i.durationMs))
644
644
  : "";
645
645
  const progress = i.status === "running" && i.progress ? ` ${style.dim(`· ${i.progress}`)}` : "";
646
- const head = `${marker}${icon} ${style.bold(humanizeToolName(i.name))} ${style.gray(i.summary)}${progress}`;
646
+ const forge = forgeLabel(i.forge);
647
+ const forgePart = forge ? ` ${style.dim(`· ${forge}`)}` : "";
648
+ const head = `${marker}${icon} ${style.bold(humanizeToolName(i.name))} ${style.gray(i.summary)}${forgePart}${progress}`;
647
649
  const gap = width - visibleWidth(head) - visibleWidth(right) - 1;
648
650
  const lines = [
649
651
  gap > 0 ? `${head}${" ".repeat(gap + 1)}${right}` : truncateToWidth(head, width),
@@ -728,7 +730,7 @@ export class ToolGroupBlock {
728
730
  : `${style.bold(member.humanName)} ${style.dim(`(${member.name})`)}`;
729
731
  lines.push(` ${micon} ${name}${member.durationMs !== undefined
730
732
  ? ` ${style.gray(`· ${formatDuration(member.durationMs)}`)}`
731
- : ""}${member.summary ? ` ${style.gray(member.summary)}` : ""}`);
733
+ : ""}${member.summary ? ` ${style.gray(member.summary)}` : ""}${member.forge ? ` ${style.dim(`· ${member.forge}`)}` : ""}`);
732
734
  const rich = richBodyLines(member, width);
733
735
  if (rich.lines.length) {
734
736
  lines.push(...rich.lines.map((line) => ` ${line}`));
@@ -1,4 +1,4 @@
1
- import type { Message, ModelInfo, RunEvent, ToolResult, TreeNode, UiBlock } from "@alisio/sdk";
1
+ import type { EngineId, Message, ModelInfo, RunEvent, ToolResult, TreeNode, UiBlock } from "@alisio/sdk";
2
2
  export type Level = "ok" | "warn" | "danger";
3
3
  export declare function formatTokens(n: number): string;
4
4
  /** Prefixes every model with its owning provider so identical model ids are never ambiguous. */
@@ -317,6 +317,8 @@ export type TranscriptItem = {
317
317
  toolKind?: ToolKind;
318
318
  /** Last non-empty `tool_progress` line while the tool runs (cleared on completion). */
319
319
  progress?: string;
320
+ /** What a Forja execution (`js_run`, `python_run`) reported; kept after completion. */
321
+ forge?: ForgeLine;
320
322
  /** Exit code derived safely from the JSON preview when the tool reports one (run_process/shell/search_text). */
321
323
  exitCode?: number;
322
324
  } | {
@@ -377,6 +379,18 @@ export declare function reasoningDurationMs(item: {
377
379
  reasoningStartedAt?: number;
378
380
  reasoningDoneAt?: number;
379
381
  }): number | undefined;
382
+ /** What the row of a Forja execution shows: nested calls, the script's own note and the engine. */
383
+ export interface ForgeLine {
384
+ engine: EngineId;
385
+ /** Highest nested call number seen. */
386
+ started: number;
387
+ /** Nested calls that ended (succeeded, failed or denied). */
388
+ finished: number;
389
+ /** The script's own `progress({ message, completed, total })`, bounded. */
390
+ note?: string;
391
+ }
392
+ /** `page 2 (2/9) · 2/3 calls · wasm`; empty when there is nothing to say yet. */
393
+ export declare function forgeLabel(forge: ForgeLine | undefined): string;
380
394
  export declare function reduceEvent(state: ViewState, event: RunEvent): ViewState;
381
395
  /** No event for this long: the footer says how long the run has been quiet. */
382
396
  export declare const QUIET_MS = 15000;
@@ -460,6 +474,8 @@ export interface ToolItemView {
460
474
  summary: string;
461
475
  /** Last progress line while running. */
462
476
  progress?: string;
477
+ /** `2/3 calls · wasm` of a Forja execution (running or finished). */
478
+ forge?: string;
463
479
  status: ToolStatus;
464
480
  durationMs?: number;
465
481
  preview?: string;
package/dist/tui/state.js CHANGED
@@ -580,6 +580,54 @@ function updateTool(state, id, update) {
580
580
  items[index] = { ...item, ...update };
581
581
  return { ...state, items };
582
582
  }
583
+ const FORGE_NOTE_MAX = 40;
584
+ const isForgeProgress = (value) => !!value &&
585
+ typeof value === "object" &&
586
+ value.kind === "forge" &&
587
+ typeof value.engine === "string";
588
+ /** Folds one `ForgeProgress` into the line (terminal call events are never coalesced away). */
589
+ function foldForgeProgress(current, progress) {
590
+ const next = current
591
+ ? { ...current, engine: progress.engine }
592
+ : { engine: progress.engine, started: 0, finished: 0 };
593
+ if (progress.call) {
594
+ next.started = Math.max(next.started, progress.call.callId);
595
+ if (progress.call.status !== "started")
596
+ next.finished += 1;
597
+ }
598
+ else if (progress.message || progress.total !== undefined) {
599
+ const fraction = progress.completed !== undefined && progress.total !== undefined
600
+ ? ` (${progress.completed}/${progress.total})`
601
+ : "";
602
+ const message = (progress.message ?? "").slice(0, FORGE_NOTE_MAX);
603
+ next.note = `${message}${fraction}`.trim();
604
+ }
605
+ return next;
606
+ }
607
+ /** The isolation tag of a Python engine; nothing for JavaScript (it is always isolated). */
608
+ const FORGE_ENGINE_TAG = {
609
+ "python-oci": "oci",
610
+ "python-wasm": "wasm",
611
+ "python-managed": "host!",
612
+ };
613
+ /** `page 2 (2/9) · 2/3 calls · wasm`; empty when there is nothing to say yet. */
614
+ export function forgeLabel(forge) {
615
+ if (!forge)
616
+ return "";
617
+ return [
618
+ forge.note,
619
+ forge.started > 0 ? `${forge.finished}/${forge.started} calls` : undefined,
620
+ FORGE_ENGINE_TAG[forge.engine],
621
+ ]
622
+ .filter(Boolean)
623
+ .join(" · ");
624
+ }
625
+ const ENGINE_LINE = /^engine: (python-oci|python-wasm|python-managed) \(/m;
626
+ /** The Python engine a result text names on its last line (PD11), for rows without progress. */
627
+ const engineOfResult = (text) => {
628
+ const engine = typeof text === "string" ? ENGINE_LINE.exec(text)?.[1] : undefined;
629
+ return engine ? { engine: engine, started: 0, finished: 0 } : undefined;
630
+ };
583
631
  /** Longest progress line kept on a running tool row. */
584
632
  const PROGRESS_LINE_MAX = 80;
585
633
  /** The last non-empty line of a `tool_progress` payload, bounded; empty when it has none. */
@@ -733,6 +781,11 @@ function reduceEventItems(state, event) {
733
781
  });
734
782
  }
735
783
  case "tool_progress": {
784
+ if (isForgeProgress(d.data)) {
785
+ const row = state.items.findLast((i) => i.kind === "tool" && i.id === d.id);
786
+ const before = row?.kind === "tool" ? row.forge : undefined;
787
+ return updateTool(state, d.id, { forge: foldForgeProgress(before, d.data) });
788
+ }
736
789
  const line = lastProgressLine(d.data);
737
790
  return line ? updateTool(state, d.id, { progress: line }) : state;
738
791
  }
@@ -757,6 +810,9 @@ function reduceEventItems(state, event) {
757
810
  status: d.isError ? "error" : "ok",
758
811
  ...(typeof d.durationMs === "number" ? { durationMs: d.durationMs } : {}),
759
812
  ...(typeof d.preview === "string" ? { preview: d.preview } : {}),
813
+ ...(running?.kind === "tool" && !running.forge && engineOfResult(d.preview)
814
+ ? { forge: engineOfResult(d.preview) }
815
+ : {}),
760
816
  ...exitCodePart(d.preview),
761
817
  ...(isUiBlock(d.ui) ? { ui: d.ui } : {}),
762
818
  ...(isImagePart(d.image) ? { image: d.image } : {}),
@@ -1186,6 +1242,7 @@ export function toolItemView(item) {
1186
1242
  kind: item.toolKind ?? toolKindOf(item.name),
1187
1243
  summary: item.summary,
1188
1244
  ...(item.progress ? { progress: item.progress } : {}),
1245
+ ...(forgeLabel(item.forge) ? { forge: forgeLabel(item.forge) } : {}),
1189
1246
  status: item.status,
1190
1247
  ...(item.durationMs !== undefined ? { durationMs: item.durationMs } : {}),
1191
1248
  ...(item.preview !== undefined ? { preview: item.preview } : {}),
@@ -1351,11 +1408,13 @@ export function itemsFromHistory(messages) {
1351
1408
  items.push({ kind: "assistant", text: m.text, reasoning: "", done: true });
1352
1409
  for (const c of m.calls) {
1353
1410
  const r = results.get(c.id);
1354
- const preview = r?.result.content
1411
+ const text = r?.result.content
1355
1412
  .filter((p) => p.type === "text")
1356
1413
  .map((x) => x.text)
1357
- .join("\n")
1358
- .slice(0, 2_000);
1414
+ .join("\n");
1415
+ const preview = text?.slice(0, 2_000);
1416
+ // The engine line closes a python_run result (PD11); read it before the preview clips it.
1417
+ const forge = c.name === "python_run" ? engineOfResult(text) : undefined;
1359
1418
  items.push({
1360
1419
  kind: "tool",
1361
1420
  id: c.id,
@@ -1365,6 +1424,7 @@ export function itemsFromHistory(messages) {
1365
1424
  status: r?.result.isError ? "error" : "ok",
1366
1425
  preview,
1367
1426
  toolKind: toolKindOf(c.name),
1427
+ ...(forge ? { forge } : {}),
1368
1428
  ...exitCodePart(preview),
1369
1429
  ...richPartsOf(r?.result, c.name),
1370
1430
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/alisio-code",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "description": "Alisio: an extensible, provider-agnostic coding-agent harness for your terminal. TUI, OpenAI-compatible providers, permissioned local tools, context compaction, persistent memory and a typed plugin SDK.",
5
5
  "author": "Gustavo Gutiérrez",
6
6
  "license": "MIT",
@@ -45,12 +45,12 @@
45
45
  "alisio": "./dist/main.js"
46
46
  },
47
47
  "dependencies": {
48
- "@alisio/core": "0.5.2",
49
- "@alisio/plugin-memory": "0.5.2",
50
- "@alisio/plugin-openai-compatible": "0.5.2",
51
- "@alisio/plugin-subagents": "0.5.2",
52
- "@alisio/sdk": "0.5.2",
53
- "@alisio/server": "0.5.2",
48
+ "@alisio/core": "0.7.0",
49
+ "@alisio/plugin-memory": "0.7.0",
50
+ "@alisio/plugin-openai-compatible": "0.7.0",
51
+ "@alisio/plugin-subagents": "0.7.0",
52
+ "@alisio/sdk": "0.7.0",
53
+ "@alisio/server": "0.7.0",
54
54
  "@earendil-works/pi-tui": "0.87.1",
55
55
  "commander": "15.0.0"
56
56
  },