@hicaru/pi-rlm 0.2.0 → 0.2.2

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.
Files changed (68) hide show
  1. package/README.md +12 -35
  2. package/README.ru.md +18 -23
  3. package/README.zh-CN.md +17 -28
  4. package/package.json +1 -1
  5. package/src/bridge/library.ts +61 -26
  6. package/src/bridge/subcall-handlers.ts +382 -0
  7. package/src/commands/rlm-config.ts +47 -18
  8. package/src/commands/rlm.ts +3 -152
  9. package/src/config/defaults.ts +7 -15
  10. package/src/config/settings.ts +8 -32
  11. package/src/context/library-context.ts +90 -17
  12. package/src/core/engine.ts +115 -360
  13. package/src/core/history.ts +1 -1
  14. package/src/core/limits.ts +5 -12
  15. package/src/core/resource-limits.ts +0 -2
  16. package/src/core/types.ts +3 -36
  17. package/src/index.ts +49 -10
  18. package/src/mode/llm-model.ts +54 -0
  19. package/src/mode/rlm-mode.ts +26 -57
  20. package/src/prompts/glossary.ts +287 -0
  21. package/src/prompts/native.ts +127 -0
  22. package/src/prompts/system.ts +14 -386
  23. package/src/sandbox/context-file.ts +154 -0
  24. package/src/sandbox/interrupts.ts +145 -0
  25. package/src/sandbox/protocol.ts +14 -69
  26. package/src/sandbox/py/guards.py +150 -0
  27. package/src/sandbox/py/retrieval.py +265 -0
  28. package/src/sandbox/py/tasks.py +116 -0
  29. package/src/sandbox/py/worker.py +836 -0
  30. package/src/sandbox/sandbox-manager.ts +33 -6
  31. package/src/sandbox/sandbox.ts +153 -182
  32. package/src/text/tokens.ts +29 -3
  33. package/src/tool/background-tasks.ts +95 -0
  34. package/src/tool/repl-details.ts +4 -2
  35. package/src/tool/repl-render.ts +58 -0
  36. package/src/tool/repl-result.ts +70 -0
  37. package/src/tool/repl-tool.ts +178 -216
  38. package/src/tool/rlm-aggregator.ts +2 -10
  39. package/src/tool/rlm-details.ts +0 -2
  40. package/src/tool/rlm-events.ts +10 -16
  41. package/src/tool/rlm-tool.ts +1 -12
  42. package/src/tool/subcall-render.ts +15 -3
  43. package/src/tool/subcall-store.ts +57 -1
  44. package/src/ui/config-panel.ts +4 -16
  45. package/src/ui/intro.ts +1 -2
  46. package/src/ui/model-picker.ts +34 -10
  47. package/src/ui/status.ts +3 -7
  48. package/src/util/concurrency.ts +91 -13
  49. package/src/util/trace.ts +42 -0
  50. package/src/bridge/fallback-todo.ts +0 -137
  51. package/src/bridge/interactive.ts +0 -65
  52. package/src/bridge/llm-query.ts +0 -156
  53. package/src/bridge/pi-interactive.ts +0 -41
  54. package/src/bridge/rlm-query.ts +0 -108
  55. package/src/core/artifacts.ts +0 -89
  56. package/src/core/critique.ts +0 -92
  57. package/src/core/gates.ts +0 -301
  58. package/src/core/pipeline-handlers.ts +0 -319
  59. package/src/core/pipeline.ts +0 -268
  60. package/src/prompts/phases.ts +0 -104
  61. package/src/sandbox/worker.py +0 -1078
  62. package/src/state/index.ts +0 -24
  63. package/src/state/internal.ts +0 -46
  64. package/src/state/paths.ts +0 -44
  65. package/src/state/reads.ts +0 -133
  66. package/src/state/resume.ts +0 -173
  67. package/src/state/rows.ts +0 -123
  68. package/src/state/writes.ts +0 -58
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Session-scoped home for detached `spawn()` work in native repl() mode.
3
+ *
4
+ * A spawned sub-call may settle after the repl() call that started it has returned, when
5
+ * that turn's RlmEmitter has been shut down and its LimitGuard discarded. Both live here for
6
+ * the whole session instead. The turn that awaits a task — or simply the next turn to run —
7
+ * adopts its settled subtree into its own ReplDetails, so background spend is reported even
8
+ * when the model never collects the result.
9
+ *
10
+ * The headless engine needs none of this: its emitter and guard already outlive every
11
+ * sub-call it services (see core/engine.ts).
12
+ */
13
+
14
+ import { RlmEmitter } from "./rlm-events.ts";
15
+ import { SubcallStore, type SubcallTotals } from "./subcall-store.ts";
16
+ import type { RlmSubcall } from "./rlm-details.ts";
17
+ import { LimitGuard, type Limits } from "../core/limits.ts";
18
+ import type { Invocation } from "../bridge/subcall-handlers.ts";
19
+ import { trace, traceEnabled } from "../util/trace.ts";
20
+
21
+ /** What a drain hands to the turn that is reporting it. */
22
+ export interface BackgroundDrain {
23
+ readonly subcalls: readonly RlmSubcall[];
24
+ readonly totals: SubcallTotals;
25
+ }
26
+
27
+ export class BackgroundTasks {
28
+ /** "bg" prefix so these IDs can never collide with a turn emitter's `s1, s2, …`. */
29
+ private readonly _emitter = new RlmEmitter("bg");
30
+ private readonly store = new SubcallStore(this._emitter);
31
+ private readonly limits: LimitGuard;
32
+ private active = 0;
33
+
34
+ constructor(limits: Limits) {
35
+ this.limits = new LimitGuard(limits);
36
+ }
37
+
38
+ /** Read-only access for the progressive tracer (scope "background"). */
39
+ get emitter(): RlmEmitter {
40
+ return this._emitter;
41
+ }
42
+
43
+ /** The Invocation detached sub-calls resolve to. Stable for the session. */
44
+ get invocation(): Invocation {
45
+ return {
46
+ emitter: this._emitter,
47
+ parentId: undefined,
48
+ depth: 0,
49
+ limits: this.limits,
50
+ };
51
+ }
52
+
53
+ /** Detached sub-calls still in flight. The single pending counter for the session. */
54
+ get pending(): number {
55
+ return this.active;
56
+ }
57
+
58
+ /** Live snapshot of detached sub-calls, settled or not — progressive rendering only. */
59
+ liveSubcalls(): readonly RlmSubcall[] {
60
+ return this.store.getSubcalls();
61
+ }
62
+
63
+ /** Live totals for that same snapshot. Accounting still flows through `drain()`. */
64
+ liveTotals(): SubcallTotals {
65
+ return this.store.getTotals();
66
+ }
67
+
68
+ /** Count `run` as in-flight detached work for its duration. */
69
+ async track<T>(run: () => Promise<T>): Promise<T> {
70
+ this.active += 1;
71
+ const startedAt = Date.now();
72
+ if (traceEnabled) trace("bg.start", { pending: this.active });
73
+ try {
74
+ return await run();
75
+ } finally {
76
+ this.active -= 1;
77
+ if (traceEnabled) trace("bg.settle", { pending: this.active, durationMs: Date.now() - startedAt });
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Hand over every fully-settled subtree and forget it.
83
+ *
84
+ * Called at the end of every repl() call, not only when a task was awaited, so a spawn
85
+ * the model never collects still reaches the user's cost totals.
86
+ */
87
+ drain(): BackgroundDrain {
88
+ return this.store.takeSettledSubtrees();
89
+ }
90
+
91
+ dispose(): void {
92
+ this.store.dispose();
93
+ this._emitter.shutdown();
94
+ }
95
+ }
@@ -2,7 +2,7 @@
2
2
  * ReplDetails — structured payload for the repl() tool's AgentToolResult<T>.
3
3
  *
4
4
  * Mirrors RlmDetails but scoped to a single code execution. Sub-calls (llm_query,
5
- * rlm_query, todo, ask_user_question) triggered during sandbox execution are
5
+ * rlm_query, load_library) triggered during sandbox execution are
6
6
  * accumulated into the subcalls array for tree rendering.
7
7
  */
8
8
 
@@ -16,12 +16,14 @@ export interface ReplDetails {
16
16
  readonly stderr: string;
17
17
  /** Wall-clock execution time in milliseconds. */
18
18
  readonly executionTimeMs: number;
19
- /** Sub-calls triggered during this execution (llm_query, rlm_query, todo, etc.). */
19
+ /** Sub-calls triggered during this execution (llm_query, rlm_query, etc.). */
20
20
  readonly subcalls: readonly RlmSubcall[];
21
21
  /** Running totals for this repl() call (cost + tokens from sub-LLM calls). */
22
22
  readonly totals: { readonly costUsd: number; readonly tokens: number };
23
23
  /** Final answer submitted through answer["ready"] without echoing it to the model. */
24
24
  readonly finalAnswer?: string;
25
+ /** Detached spawn() sub-calls still running when this call returned. Absent when none. */
26
+ readonly backgroundPending?: number;
25
27
  /** Advisory diagnostics — surfaced to the user, never a failure. */
26
28
  readonly warnings?: readonly string[];
27
29
  }
@@ -0,0 +1,58 @@
1
+ /** repl() tool TUI views — collapsed one-liner card and the expanded output/sub-call tree. */
2
+
3
+ import type { Theme } from "@earendil-works/pi-coding-agent";
4
+ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
5
+ import type { ReplDetails } from "./repl-details.ts";
6
+ import { cardHeader, cardStatsLine, renderCollapsedCard, renderExpandedSubcallTree } from "./subcall-render.ts";
7
+
8
+ /** Chars of stdout/stderr shown in the expanded view. */
9
+ const EXPANDED_STDOUT_CHARS = 2_000;
10
+ const EXPANDED_STDERR_CHARS = 500;
11
+
12
+ // ── Collapsed view ──
13
+
14
+ export function replStats(details: ReplDetails, theme: Theme): string {
15
+ const elapsed = details.executionTimeMs > 0 ? `${details.executionTimeMs}ms` : undefined;
16
+ return cardStatsLine(details.totals, theme, elapsed, details.backgroundPending);
17
+ }
18
+
19
+ export function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
20
+ return renderCollapsedCard("REPL", details.status, replStats(details, theme), details.subcalls, theme);
21
+ }
22
+
23
+ // ── Expanded view ──
24
+
25
+ export function renderReplExpanded(details: ReplDetails, theme: Theme): Container {
26
+ const container = new Container();
27
+
28
+ container.addChild(new Text(cardHeader("REPL", details.status, replStats(details, theme), theme), 0, 0));
29
+
30
+ // Output
31
+ if (details.output) {
32
+ container.addChild(new Spacer(1));
33
+ const out = details.output.length > EXPANDED_STDOUT_CHARS
34
+ ? `${details.output.slice(0, EXPANDED_STDOUT_CHARS)}…`
35
+ : details.output;
36
+ container.addChild(new Text(out, 0, 0));
37
+ }
38
+
39
+ if (details.warnings && details.warnings.length > 0) {
40
+ container.addChild(new Spacer(1));
41
+ container.addChild(new Text(theme.fg("muted", details.warnings.join("\n")), 0, 0));
42
+ }
43
+
44
+ // Stderr
45
+ if (details.stderr) {
46
+ container.addChild(new Spacer(1));
47
+ container.addChild(new Text(theme.fg("error", details.stderr.slice(0, EXPANDED_STDERR_CHARS)), 0, 0));
48
+ }
49
+
50
+ // Sub-call tree
51
+ if (details.subcalls.length > 0) {
52
+ container.addChild(new Spacer(1));
53
+ container.addChild(new Text(theme.fg("muted", "─── Sub-calls ───"), 0, 0));
54
+ container.addChild(renderExpandedSubcallTree(details.subcalls, theme));
55
+ }
56
+
57
+ return container;
58
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Model-visible text assembly for a repl() result, plus the advisory diagnostics derived from
3
+ * its sub-calls. Split out of repl-tool.ts: this is pure string/array work with no sandbox,
4
+ * emitter, or TUI dependency, and both halves are asserted directly by test/phase-guards.ts.
5
+ */
6
+
7
+ import type { RlmSubcall } from "./rlm-details.ts";
8
+ import { capReplResultText, replDelegationNudge } from "../mode/native-guards.ts";
9
+
10
+ /** Model-visible text assembled from a repl() result. */
11
+ export interface ReplResultText {
12
+ readonly text: string;
13
+ }
14
+
15
+ /**
16
+ * Assemble the model-visible text for a repl() result: cap stdout, append a zero-subcall
17
+ * delegation nudge when a bulk read went undelegated, and report tasks still running.
18
+ *
19
+ * The pending line is the model's only signal that `spawn()`ed work is outstanding — without
20
+ * it a model that spawned and moved on has no way to know it should still collect.
21
+ *
22
+ * `varNames` covers the opposite failure: a block that stores its results in `answers` and
23
+ * prints nothing reads as a bare "(no output)", so the model concludes the block did nothing
24
+ * and re-runs it — paying twice for the same sub-calls. The headless engine already answers
25
+ * this with the same hint (core/answer.ts); native mode was the only path missing it.
26
+ */
27
+ export function buildReplResultText(
28
+ stdout: string,
29
+ finalAnswer: string | undefined,
30
+ subcalls: readonly RlmSubcall[],
31
+ backgroundPending = 0,
32
+ varNames: readonly string[] = [],
33
+ ): ReplResultText {
34
+ const answerSubmitted = finalAnswer !== undefined;
35
+ const noOutput = !answerSubmitted && !stdout;
36
+ const varsHint = noOutput && varNames.length > 0
37
+ ? ` — the block ran fine and these REPL vars are defined: ${varNames.join(", ")}. `
38
+ + "Do NOT re-run it; read them in the next block."
39
+ : "";
40
+ const rawText = answerSubmitted
41
+ ? `ANSWER_SUBMITTED (${finalAnswer.length} chars) — delivered to user. Do not restate it.`
42
+ : stdout || `(no output)${varsHint}`;
43
+ // Model-visible text is capped; the caller keeps full stdout in `details` for the TUI.
44
+ const cappedText = capReplResultText(rawText) ?? rawText;
45
+ const delegated = subcalls.some((s) => s.kind === "llm" || s.kind === "batch" || s.kind === "rlm");
46
+ const nudge = answerSubmitted ? undefined : replDelegationNudge(rawText.length, delegated);
47
+ const failedBg = subcalls.filter((s) => s.id.startsWith("bg") && s.status === "error").length;
48
+ const pendingLine = backgroundPending > 0
49
+ ? `\n\n[rlm] ${backgroundPending} background task(s) still running — rlm_await_all(tasks) to collect.`
50
+ : "";
51
+ const failedLine = failedBg > 0
52
+ ? `\n[rlm] ${failedBg} background sub-call(s) FAILED — their rlm_await value is an "Error: …" string, not data.`
53
+ : "";
54
+ return { text: cappedText + (nudge ?? "") + pendingLine + failedLine };
55
+ }
56
+
57
+ /** Advisory diagnostics derived from a completed invocation's sub-calls. */
58
+ export function collectReplWarnings(subcalls: readonly RlmSubcall[]): readonly string[] | undefined {
59
+ let failed = 0;
60
+ let total = 0;
61
+ for (let i = 0; i < subcalls.length; i++) {
62
+ const call = subcalls[i];
63
+ if (call.status !== "error") continue;
64
+ // A batch subcall stands for many prompts; a single call stands for one.
65
+ failed += call.failedCount ?? 1;
66
+ total += call.totalCount ?? 1;
67
+ }
68
+ if (failed === 0) return undefined;
69
+ return Object.freeze([`${failed}/${total} sub-call(s) failed — results may be incomplete`]);
70
+ }