@alisio/alisio-code 0.1.0-alpha.26 → 0.1.0-alpha.28

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/main.js CHANGED
@@ -127,6 +127,14 @@ async function run(cmd, prompt, sessionId) {
127
127
  process.stdout.write(`${JSON.stringify(event)}\n`);
128
128
  else if (event.type === "text_delta")
129
129
  process.stdout.write(String(event.data.delta));
130
+ else if (event.type === "request_retry") {
131
+ const retry = event.data;
132
+ process.stderr.write(`\nThe model did not respond; retrying (${retry.attempt}/${retry.of})…\n`);
133
+ }
134
+ else if (event.type === "truncation_recovery") {
135
+ const recovery = event.data;
136
+ process.stderr.write(`\nThe response was cut off by the output limit; retrying in smaller steps (${recovery.attempt}/${recovery.of})…\n`);
137
+ }
130
138
  else if (event.type === "tool_started")
131
139
  process.stderr.write(`\n→ ${event.data.name}\n`);
132
140
  else if (event.type === "artifact_published")
@@ -314,7 +322,7 @@ program
314
322
  console.log('To generate AGENTS.md for this project, run /init inside alisio (or: alisio run "/init" --allow-write).');
315
323
  });
316
324
  program.command("doctor").action(async (_opts, cmd) => {
317
- const { findWorkspace, loadConfigWithProvenance, overridesSavedProviderProfile, ProviderSettingsStore, RIPGREP_INSTALL_HINT, which, } = await import("@alisio/core");
325
+ const { DEFAULT_MAX_OUTPUT_TOKENS, findWorkspace, loadConfigWithProvenance, MAX_AUTO_OUTPUT_TOKENS, overridesSavedProviderProfile, ProviderSettingsStore, RIPGREP_INSTALL_HINT, which, } = await import("@alisio/core");
318
326
  const o = options(cmd);
319
327
  const workspace = await findWorkspace(o.cwd ?? process.cwd());
320
328
  const { config, provenance } = await loadConfigWithProvenance(workspace, {
@@ -337,6 +345,14 @@ program.command("doctor").action(async (_opts, cmd) => {
337
345
  workspace,
338
346
  git: (await which("git")) ?? null,
339
347
  ripgrep: (await which("rg")) ?? null,
348
+ // The per-request output budget is resolved when a run starts, from the model's catalog;
349
+ // doctor does not contact the provider, so it reports what the configuration decides.
350
+ outputTokens: config.limits.maxOutputTokens !== undefined
351
+ ? { limit: config.limits.maxOutputTokens, source: "set by you (limits.maxOutputTokens)" }
352
+ : {
353
+ limit: null,
354
+ source: `the model catalog's declared limit (capped at ${MAX_AUTO_OUTPUT_TOKENS}), else ${DEFAULT_MAX_OUTPUT_TOKENS}`,
355
+ },
340
356
  provider: {
341
357
  id: useSaved ? saved.profile.provider : "openai-compatible",
342
358
  baseURL: useSaved ? saved.profile.values.baseURL : config.provider.baseURL,
@@ -6,7 +6,7 @@ import { attachmentCaption, MAX_ATTACHMENTS_PER_MESSAGE, } from "./attachments.j
6
6
  import { btwCurrent, btwHints, btwPosition, btwUsageLine, btwWindow, reduceBtw, } from "./btw.js";
7
7
  import { branchDisplay } from "./git-branch.js";
8
8
  import { initialQuestionState, reduceQuestions, } from "./questions.js";
9
- import { contextLevel, contextPercent, editSummary, entryKeyOf, fitIdentityParts, fitSegments, foldCandidateOf, formatContext, formatDuration, formatTokens, groupIdentity, groupToolEntries, humanizeToolName, isUiBlock, previewLinesFor, previewRows, REASONING_EXPAND_MAX_LINES, reasoningDurationMs, } from "./state.js";
9
+ 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";
10
10
  import { highlightCode, imageTheme, levelColor, markdownDefaultTextStyle, markdownTheme, markdownTransform, sanitizeCode, style, } from "./theme.js";
11
11
  export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
12
12
  /** Shared animation clock advanced by the app while work is running. */
@@ -437,10 +437,14 @@ export class Footer {
437
437
  const bar = pct === undefined
438
438
  ? style.gray("░".repeat(cells))
439
439
  : levelColor(level)("█".repeat(filled)) + style.gray("░".repeat(cells - filled));
440
+ const quiet = v.streaming ? quietFor(v, clock.now) : 0;
441
+ const quietNote = quiet
442
+ ? ` · no response for ${formatDuration(quiet)}${quiet >= STALLED_MS ? " (stuck?)" : ""}`
443
+ : "";
440
444
  const state = v.compacting
441
445
  ? `${SPINNER[clock.frame % SPINNER.length]} compacting`
442
446
  : v.streaming
443
- ? `${SPINNER[clock.frame % SPINNER.length]} streaming ${formatDuration(clock.now - (v.runStartedAt ?? clock.now))}`
447
+ ? `${SPINNER[clock.frame % SPINNER.length]} ${runPhase(v).label} ${formatDuration(clock.now - (v.runStartedAt ?? clock.now))}${quietNote}`
444
448
  : "● idle";
445
449
  const s = v.stats;
446
450
  const segments = [
@@ -449,7 +453,15 @@ export class Footer {
449
453
  priority: 10,
450
454
  paint: pct === undefined ? style.gray : levelColor(level),
451
455
  },
452
- { text: state, priority: 9, paint: v.streaming || v.compacting ? style.cyan : style.green },
456
+ {
457
+ text: state,
458
+ priority: 9,
459
+ paint: quiet >= STALLED_MS
460
+ ? style.yellow
461
+ : v.streaming || v.compacting
462
+ ? style.cyan
463
+ : style.green,
464
+ },
453
465
  {
454
466
  text: `↑${formatTokens(s.input)} ↓${formatTokens(s.output)}${s.cached ? ` ⚡${formatTokens(s.cached)}` : ""}`,
455
467
  priority: 6,
@@ -50,7 +50,8 @@ export interface SettingsConfigView {
50
50
  };
51
51
  limits: {
52
52
  maxTurns: number;
53
- maxOutputTokens: number;
53
+ /** Only present when the user set it; unset means "the model's declared limit, else 16384". */
54
+ maxOutputTokens?: number | undefined;
54
55
  maxContextChars: number;
55
56
  timeoutMs: number;
56
57
  };
@@ -12,7 +12,7 @@ export const WEBSEARCH_PROVIDERS = [
12
12
  export const defaultConfig = {
13
13
  compaction: { auto: true, threshold: 0.85, keepTurns: 2, maxOutputTokens: 16_000 },
14
14
  context: { claudeMdFallback: false, maxBytes: 32 * 1024 },
15
- limits: { maxTurns: 100, maxOutputTokens: 16_384, maxContextChars: 800_000, timeoutMs: 300_000 },
15
+ limits: { maxTurns: 100, maxContextChars: 800_000, timeoutMs: 300_000 },
16
16
  tui: { paddingX: 1, contentPaddingX: 2, skillSlashCommands: true },
17
17
  mcp: { allow: false },
18
18
  websearch: { provider: undefined },
@@ -125,9 +125,10 @@ export const SETTINGS_DEFINITIONS = [
125
125
  label: "Agent max output tokens",
126
126
  category: "Limits",
127
127
  valueType: "number",
128
- values: [1_024, 2_048, 4_096, 8_192, 16_384],
129
- read: (config) => config.limits.maxOutputTokens,
130
- description: "Per-call output token budget for agent turns. A cut response shows a notice suggesting a higher value. Applied from the next run.",
128
+ values: [1_024, 2_048, 4_096, 8_192, 16_384, 32_768, 65_536],
129
+ // Unset: each request uses the model's declared limit (capped at 65536), else 16384.
130
+ read: (config) => config.limits.maxOutputTokens ?? 16_384,
131
+ description: "Per-call output token budget for agent turns. While you leave it unchanged, each request uses the output limit the model's catalog declares (capped at 65536), or 16384 when it declares none; a value set here always wins. A cut response shows a notice suggesting a higher value. Applied from the next run.",
131
132
  },
132
133
  {
133
134
  id: "limits.maxContextChars",
@@ -343,6 +343,16 @@ export interface ViewState {
343
343
  items: TranscriptItem[];
344
344
  streaming: boolean;
345
345
  compacting: boolean;
346
+ /** The runner is sending a silent request again (`request_retry`); cleared by any output. */
347
+ retry?: {
348
+ attempt: number;
349
+ of: number;
350
+ };
351
+ /** The response was cut by the output limit and is requested again (`truncation_recovery`). */
352
+ recovery?: {
353
+ attempt: number;
354
+ of: number;
355
+ };
346
356
  model: string;
347
357
  /** Context size in tokens: provider-reported, or estimated (`~`). */
348
358
  context?: {
@@ -350,6 +360,8 @@ export interface ViewState {
350
360
  estimated: boolean;
351
361
  };
352
362
  runStartedAt?: number;
363
+ /** Epoch ms of the latest event of the live run (any type); undefined when no run is live. */
364
+ lastEventAt?: number;
353
365
  stats: Stats;
354
366
  }
355
367
  export declare function initialViewState(model: string, now?: number): ViewState;
@@ -360,6 +372,19 @@ export declare function reasoningDurationMs(item: {
360
372
  reasoningDoneAt?: number;
361
373
  }): number | undefined;
362
374
  export declare function reduceEvent(state: ViewState, event: RunEvent): ViewState;
375
+ /** No event for this long: the footer says how long the run has been quiet. */
376
+ export declare const QUIET_MS = 15000;
377
+ /** No event for this long: the footer marks the run as possibly stuck. */
378
+ export declare const STALLED_MS = 60000;
379
+ export interface RunPhase {
380
+ label: string;
381
+ /** Silence is expected here (the user's turn, or a sub-agent working in its own session). */
382
+ silentByDesign: boolean;
383
+ }
384
+ /** What the live run is doing, from the transcript items and flags the events already update. */
385
+ export declare function runPhase(view: ViewState): RunPhase;
386
+ /** Milliseconds of silence worth reporting (0 when the run is busy, or silent by design). */
387
+ export declare function quietFor(view: ViewState, now: number): number;
363
388
  export declare function lastAssistantText(items: TranscriptItem[]): string | undefined;
364
389
  /**
365
390
  * True when a printable key should copy the last assistant response instead of typing: bound to
package/dist/tui/state.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Pure presentation logic for the TUI: formatting, command parsing and the reduction of
3
3
  * versioned runner events into a view model. No terminal or pi-tui imports here.
4
4
  */
5
- import { CommandCatalog } from "@alisio/core";
5
+ import { CommandCatalog, describeOutputLimitSource } from "@alisio/core";
6
6
  const trimZero = (value) => value.replace(/\.0$/, "");
7
7
  export function formatTokens(n) {
8
8
  if (!Number.isFinite(n) || n < 0)
@@ -577,6 +577,96 @@ function updateTool(state, id, update) {
577
577
  return { ...state, items };
578
578
  }
579
579
  export function reduceEvent(state, event) {
580
+ const next = reduceEventItems(state, event);
581
+ const terminal = event.type === "run_completed" ||
582
+ event.type === "run_failed" ||
583
+ event.type === "run_cancelled" ||
584
+ event.type === "run_turns_exceeded";
585
+ if (terminal) {
586
+ const { lastEventAt: _ended, retry: _retry, recovery: _recovery, ...rest } = next;
587
+ return rest;
588
+ }
589
+ return { ...next, lastEventAt: Date.parse(event.timestamp) || Date.now() };
590
+ }
591
+ /** No event for this long: the footer says how long the run has been quiet. */
592
+ export const QUIET_MS = 15_000;
593
+ /** No event for this long: the footer marks the run as possibly stuck. */
594
+ export const STALLED_MS = 60_000;
595
+ /** A short, safe label from tool arguments: a title or a file name, never commands or URLs. */
596
+ function shortArg(args) {
597
+ const input = parseArgs(args);
598
+ if (!input)
599
+ return "";
600
+ const first = Array.isArray(input.inputs) ? input.inputs[0] : {};
601
+ const value = [input.title, input.fileName, input.path, input.file_path, first?.path].find((v) => typeof v === "string" && v.trim() !== "");
602
+ if (!value)
603
+ return "";
604
+ return truncatePlain(oneLine(value.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? ""), 40);
605
+ }
606
+ /** What the live run is doing, from the transcript items and flags the events already update. */
607
+ export function runPhase(view) {
608
+ if (view.compacting)
609
+ return { label: "compacting context", silentByDesign: false };
610
+ if (view.retry)
611
+ return {
612
+ label: `the model did not respond; retrying (${view.retry.attempt}/${view.retry.of})`,
613
+ silentByDesign: false,
614
+ };
615
+ if (view.recovery)
616
+ return {
617
+ label: `the response was cut off; retrying in smaller steps (${view.recovery.attempt}/${view.recovery.of})`,
618
+ silentByDesign: false,
619
+ };
620
+ const tool = view.items.findLast((i) => i.kind === "tool" && i.status !== "ok" && i.status !== "error");
621
+ if (tool?.kind === "tool") {
622
+ const name = tool.name.replace(/^p_[0-9a-f]{10}_/, "");
623
+ const arg = shortArg(tool.args);
624
+ const detail = arg ? ` (${arg})` : "";
625
+ if (tool.status === "approval")
626
+ return {
627
+ label: `waiting for your approval: ${humanizeToolName(name)}`,
628
+ silentByDesign: true,
629
+ };
630
+ if (name === "ask_user_question")
631
+ return { label: "waiting for your answer", silentByDesign: true };
632
+ if (name === "task" || name === "task_wait")
633
+ return { label: "a sub-agent is working", silentByDesign: true };
634
+ if (name === "python_run")
635
+ return { label: `running Python${detail}`, silentByDesign: false };
636
+ if (name === "data_inspect" || name === "data_query")
637
+ return { label: `reading your data${detail}`, silentByDesign: false };
638
+ if (name === "artifact_create")
639
+ return { label: `publishing the artifact${detail}`, silentByDesign: false };
640
+ return { label: `running ${humanizeToolName(name)}${detail}`, silentByDesign: false };
641
+ }
642
+ const last = view.items.at(-1);
643
+ if (last?.kind === "assistant" && !last.done)
644
+ return {
645
+ label: last.text ? "writing the answer" : "thinking",
646
+ silentByDesign: false,
647
+ };
648
+ return { label: "waiting for the model", silentByDesign: false };
649
+ }
650
+ /** Milliseconds of silence worth reporting (0 when the run is busy, or silent by design). */
651
+ export function quietFor(view, now) {
652
+ if (view.lastEventAt === undefined || runPhase(view).silentByDesign)
653
+ return 0;
654
+ const idle = Math.max(0, now - view.lastEventAt);
655
+ return idle >= QUIET_MS ? idle : 0;
656
+ }
657
+ /** What the retried request produced (or the end of the turn) ends the retry label. */
658
+ const RETRY_ENDERS = new Set([
659
+ "text_delta",
660
+ "reasoning_delta",
661
+ "turn_completed",
662
+ "tool_started",
663
+ "compaction_started",
664
+ ]);
665
+ function reduceEventItems(state, event) {
666
+ if ((state.retry || state.recovery) && RETRY_ENDERS.has(event.type)) {
667
+ const { retry: _retry, recovery: _recovery, ...rest } = state;
668
+ return reduceEventItems(rest, event);
669
+ }
580
670
  const d = (event.data ?? {});
581
671
  const at = Date.parse(event.timestamp);
582
672
  switch (event.type) {
@@ -679,6 +769,10 @@ export function reduceEvent(state, event) {
679
769
  }
680
770
  return ended;
681
771
  }
772
+ case "truncation_recovery":
773
+ return { ...state, recovery: { attempt: Number(d.attempt) || 1, of: Number(d.of) || 1 } };
774
+ case "request_retry":
775
+ return { ...state, retry: { attempt: Number(d.attempt) || 1, of: Number(d.of) || 1 } };
682
776
  case "compaction_started":
683
777
  return { ...state, compacting: true };
684
778
  case "compaction_completed": {
@@ -720,7 +814,9 @@ export function reduceEvent(state, event) {
720
814
  case "response_truncated":
721
815
  return addItem(state, {
722
816
  kind: "notice",
723
- text: "Response cut by max output tokens — the answer may be incomplete. Raise limits.maxOutputTokens (/settings → Agent max output tokens) to allow longer answers.",
817
+ text: `Response cut by max output tokens${typeof d.maxOutputTokens === "number"
818
+ ? ` (${d.maxOutputTokens} tokens, ${describeOutputLimitSource(d.source)})`
819
+ : ""} — the answer may be incomplete. Raise limits.maxOutputTokens (/settings → Agent max output tokens) to allow longer answers.`,
724
820
  });
725
821
  case "model_changed":
726
822
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/alisio-code",
3
- "version": "0.1.0-alpha.26",
3
+ "version": "0.1.0-alpha.28",
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.1.0-alpha.22",
48
+ "@alisio/core": "0.1.0-alpha.24",
49
49
  "@alisio/plugin-memory": "0.1.0-alpha.9",
50
- "@alisio/plugin-openai-compatible": "0.1.0-alpha.9",
50
+ "@alisio/plugin-openai-compatible": "0.1.0-alpha.10",
51
51
  "@alisio/plugin-subagents": "0.1.0-alpha.11",
52
- "@alisio/sdk": "0.1.0-alpha.18",
53
- "@alisio/server": "0.1.0-alpha.6",
52
+ "@alisio/sdk": "0.1.0-alpha.20",
53
+ "@alisio/server": "0.1.0-alpha.8",
54
54
  "@earendil-works/pi-tui": "0.87.1",
55
55
  "commander": "15.0.0"
56
56
  },