@oh-my-pi/pi-coding-agent 16.1.12 → 16.1.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli.js +5650 -2860
  3. package/dist/types/config/settings-schema.d.ts +44 -14
  4. package/dist/types/cursor.d.ts +9 -9
  5. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
  6. package/dist/types/eval/backend-helpers.d.ts +23 -0
  7. package/dist/types/eval/executor-base.d.ts +118 -0
  8. package/dist/types/eval/index.d.ts +2 -0
  9. package/dist/types/eval/jl/executor.d.ts +44 -0
  10. package/dist/types/eval/jl/index.d.ts +11 -0
  11. package/dist/types/eval/jl/kernel.d.ts +28 -0
  12. package/dist/types/eval/jl/prelude.d.ts +1 -0
  13. package/dist/types/eval/jl/runtime.d.ts +22 -0
  14. package/dist/types/eval/kernel-base.d.ts +105 -0
  15. package/dist/types/eval/py/kernel.d.ts +3 -61
  16. package/dist/types/eval/rb/executor.d.ts +77 -0
  17. package/dist/types/eval/rb/index.d.ts +11 -0
  18. package/dist/types/eval/rb/kernel.d.ts +31 -0
  19. package/dist/types/eval/rb/prelude.d.ts +1 -0
  20. package/dist/types/eval/rb/runtime.d.ts +23 -0
  21. package/dist/types/eval/runtime-env.d.ts +24 -0
  22. package/dist/types/eval/types.d.ts +3 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -15
  24. package/dist/types/extensibility/hooks/loader.d.ts +1 -25
  25. package/dist/types/extensibility/session-handler-types.d.ts +23 -0
  26. package/dist/types/jsonrpc/message-framing.d.ts +35 -0
  27. package/dist/types/mnemopi/embed-client.d.ts +7 -24
  28. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
  29. package/dist/types/modes/components/custom-editor.d.ts +11 -0
  30. package/dist/types/modes/components/selector-helpers.d.ts +53 -0
  31. package/dist/types/modes/interactive-mode.d.ts +0 -2
  32. package/dist/types/modes/theme/theme.d.ts +8 -1
  33. package/dist/types/modes/types.d.ts +0 -2
  34. package/dist/types/modes/utils/copy-targets.d.ts +1 -1
  35. package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
  36. package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
  37. package/dist/types/sdk.d.ts +1 -1
  38. package/dist/types/session/agent-session.d.ts +2 -2
  39. package/dist/types/stt/asr-client.d.ts +3 -29
  40. package/dist/types/subprocess/worker-client.d.ts +149 -0
  41. package/dist/types/subprocess/worker-runtime.d.ts +107 -0
  42. package/dist/types/tiny/title-client.d.ts +14 -34
  43. package/dist/types/tools/eval-backends.d.ts +6 -3
  44. package/dist/types/tools/eval-render.d.ts +6 -5
  45. package/dist/types/tools/eval.d.ts +13 -15
  46. package/dist/types/tools/index.d.ts +3 -3
  47. package/dist/types/tts/tts-client.d.ts +3 -28
  48. package/dist/types/tui/code-cell.d.ts +7 -0
  49. package/dist/types/utils/file-display-mode.d.ts +1 -1
  50. package/dist/types/web/parallel.d.ts +6 -0
  51. package/package.json +12 -12
  52. package/src/config/settings-schema.ts +50 -18
  53. package/src/config/settings.ts +5 -0
  54. package/src/dap/client.ts +13 -107
  55. package/src/eval/__tests__/julia-prelude.test.ts +77 -0
  56. package/src/eval/backend-helpers.ts +48 -0
  57. package/src/eval/executor-base.ts +425 -0
  58. package/src/eval/index.ts +2 -0
  59. package/src/eval/jl/executor.ts +540 -0
  60. package/src/eval/jl/index.ts +54 -0
  61. package/src/eval/jl/kernel.ts +235 -0
  62. package/src/eval/jl/prelude.jl +930 -0
  63. package/src/eval/jl/prelude.ts +3 -0
  64. package/src/eval/jl/runner.jl +634 -0
  65. package/src/eval/jl/runtime.ts +118 -0
  66. package/src/eval/js/index.ts +3 -14
  67. package/src/eval/kernel-base.ts +569 -0
  68. package/src/eval/py/executor.ts +43 -252
  69. package/src/eval/py/index.ts +9 -20
  70. package/src/eval/py/kernel.ts +29 -544
  71. package/src/eval/rb/executor.ts +504 -0
  72. package/src/eval/rb/index.ts +54 -0
  73. package/src/eval/rb/kernel.ts +230 -0
  74. package/src/eval/rb/prelude.rb +721 -0
  75. package/src/eval/rb/prelude.ts +3 -0
  76. package/src/eval/rb/runner.rb +474 -0
  77. package/src/eval/rb/runtime.ts +132 -0
  78. package/src/eval/runtime-env.ts +104 -0
  79. package/src/eval/types.ts +3 -3
  80. package/src/extensibility/extensions/runner.ts +4 -11
  81. package/src/extensibility/hooks/loader.ts +3 -21
  82. package/src/extensibility/session-handler-types.ts +21 -0
  83. package/src/internal-urls/docs-index.generated.txt +1 -1
  84. package/src/jsonrpc/message-framing.ts +142 -0
  85. package/src/lsp/client.ts +13 -109
  86. package/src/mnemopi/embed-client.ts +43 -198
  87. package/src/modes/components/agent-dashboard.ts +17 -40
  88. package/src/modes/components/chat-transcript-builder.ts +18 -102
  89. package/src/modes/components/custom-editor.ts +19 -0
  90. package/src/modes/components/extensions/extension-dashboard.ts +4 -13
  91. package/src/modes/components/extensions/extension-list.ts +16 -44
  92. package/src/modes/components/history-search.ts +4 -16
  93. package/src/modes/components/selector-helpers.ts +129 -0
  94. package/src/modes/components/settings-selector.ts +7 -5
  95. package/src/modes/components/tree-selector.ts +13 -18
  96. package/src/modes/controllers/event-controller.ts +3 -9
  97. package/src/modes/controllers/input-controller.ts +32 -54
  98. package/src/modes/interactive-mode.ts +5 -7
  99. package/src/modes/theme/theme.ts +35 -0
  100. package/src/modes/types.ts +0 -2
  101. package/src/modes/utils/copy-targets.ts +3 -2
  102. package/src/modes/utils/interactive-context-helpers.ts +27 -0
  103. package/src/modes/utils/transcript-render-helpers.ts +157 -0
  104. package/src/modes/utils/ui-helpers.ts +21 -126
  105. package/src/prompts/system/system-prompt.md +10 -10
  106. package/src/prompts/tools/bash.md +0 -1
  107. package/src/prompts/tools/eval.md +6 -4
  108. package/src/prompts/tools/find.md +0 -4
  109. package/src/prompts/tools/read.md +1 -2
  110. package/src/prompts/tools/replace.md +1 -1
  111. package/src/prompts/tools/search.md +0 -1
  112. package/src/sdk.ts +13 -7
  113. package/src/session/agent-session.ts +9 -7
  114. package/src/stt/asr-client.ts +35 -215
  115. package/src/stt/asr-worker.ts +29 -181
  116. package/src/subprocess/worker-client.ts +297 -0
  117. package/src/subprocess/worker-runtime.ts +277 -0
  118. package/src/task/executor.ts +4 -4
  119. package/src/tiny/title-client.ts +53 -219
  120. package/src/tiny/worker.ts +29 -180
  121. package/src/tools/eval-backends.ts +10 -3
  122. package/src/tools/eval-render.ts +17 -8
  123. package/src/tools/eval.ts +187 -22
  124. package/src/tools/index.ts +51 -28
  125. package/src/tts/tts-client.ts +38 -206
  126. package/src/tts/tts-worker.ts +23 -97
  127. package/src/tui/code-cell.ts +12 -1
  128. package/src/utils/file-display-mode.ts +2 -3
  129. package/src/web/parallel.ts +43 -42
  130. package/src/web/search/providers/parallel.ts +10 -99
@@ -2,11 +2,12 @@
2
2
  * TUI rendering for the eval tool.
3
3
  *
4
4
  * Split out from `eval.ts` so the renderer can be imported by `renderers.ts`
5
- * without dragging the eval *runtime* (JS/Python backends -> agent bridge ->
6
- * task executor -> sdk -> extension loader -> root barrel) into the renderer
7
- * module graph. That transitive chain re-enters `renderers.ts` while `eval.ts`
8
- * is still initializing, which previously crashed module load with a TDZ
9
- * `Cannot access 'evalToolRenderer' before initialization`.
5
+ * without dragging the eval *runtime* (JS/Python/Ruby/Julia backends ->
6
+ * agent bridge -> task executor -> sdk -> extension loader -> root barrel)
7
+ * into the renderer module graph. That transitive chain re-enters
8
+ * `renderers.ts` while `eval.ts` is still initializing, which previously
9
+ * crashed module load with a TDZ `Cannot access 'evalToolRenderer' before
10
+ * initialization`.
10
11
  */
11
12
  import type { Component } from "@oh-my-pi/pi-tui";
12
13
  import { Markdown, Text } from "@oh-my-pi/pi-tui";
@@ -41,8 +42,11 @@ import {
41
42
  } from "./render-utils";
42
43
  export const EVAL_DEFAULT_PREVIEW_LINES = 10;
43
44
 
44
- function languageForHighlighter(language: EvalLanguage | undefined): "python" | "javascript" {
45
- return language === "js" ? "javascript" : "python";
45
+ function languageForHighlighter(language: EvalLanguage | undefined): "python" | "javascript" | "ruby" | "julia" {
46
+ if (language === "js") return "javascript";
47
+ if (language === "ruby") return "ruby";
48
+ if (language === "julia") return "julia";
49
+ return "python";
46
50
  }
47
51
 
48
52
  interface EvalRenderCellArg {
@@ -70,7 +74,10 @@ interface EvalRenderCell {
70
74
  }
71
75
 
72
76
  function normalizeRenderLanguage(value: string | undefined): EvalLanguage {
73
- return value === "js" ? "js" : "python";
77
+ if (value === "js") return "js";
78
+ if (value === "rb" || value === "ruby") return "ruby";
79
+ if (value === "jl" || value === "julia") return "julia";
80
+ return "python";
74
81
  }
75
82
 
76
83
  function getRenderCells(args: EvalRenderArgs | undefined): EvalRenderCell[] {
@@ -506,6 +513,7 @@ export const evalToolRenderer = {
506
513
  {
507
514
  code: cell.code,
508
515
  language: languageForHighlighter(cell.language),
516
+ showLanguage: true,
509
517
  index: i,
510
518
  total: cells.length,
511
519
  title: cell.title,
@@ -608,6 +616,7 @@ export const evalToolRenderer = {
608
616
  {
609
617
  code: cell.code,
610
618
  language: languageForHighlighter(cell.language ?? details?.language),
619
+ showLanguage: true,
611
620
  index: i,
612
621
  total: cellResults.length,
613
622
  title: cell.title,
package/src/tools/eval.ts CHANGED
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { ImageContent, ToolExample } from "@oh-my-pi/pi-ai";
3
3
  import { prompt } from "@oh-my-pi/pi-utils";
4
4
  import { type } from "arktype";
5
- import { jsBackend, pythonBackend } from "../eval";
5
+ import { jsBackend, juliaBackend, pythonBackend, rubyBackend } from "../eval";
6
6
  import type { ExecutorBackend, ExecutorBackendResult } from "../eval/backend";
7
7
  import { EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP } from "../eval/bridge-timeout";
8
8
  import { IdleTimeout } from "../eval/idle-timeout";
@@ -14,7 +14,7 @@ import { webpExclusionForModel } from "../utils/image-loading";
14
14
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
15
15
  import type { ToolSession } from ".";
16
16
  import { truncateForPrompt } from "./approval";
17
- import { resolveEvalBackends } from "./eval-backends";
17
+ import { type EvalBackendsAllowance, resolveEvalBackends } from "./eval-backends";
18
18
  import { upsertStatusEvent } from "./eval-render";
19
19
  import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "./output-meta";
20
20
  import { ToolAbortError, ToolError } from "./tool-errors";
@@ -23,29 +23,112 @@ import { clampTimeout } from "./tool-timeouts";
23
23
 
24
24
  export { EVAL_DEFAULT_PREVIEW_LINES, evalToolRenderer } from "./eval-render";
25
25
 
26
- /**
27
- * Per-cell input. Each cell runs in order; state persists within a language
28
- * across cells and across tool calls.
29
- */
30
- const evalCellSchema = type({
31
- language: type("'py' | 'js'").describe('runtime: "py" for the IPython kernel, "js" for the persistent JS VM'),
32
- code: type("string").describe("cell body, verbatim. Use top-level await freely."),
26
+ /** Language tokens the eval tool accepts, in stable display order. */
27
+ export type EvalLanguageToken = "py" | "js" | "rb" | "jl";
28
+ const EVAL_LANGUAGE_ORDER: readonly EvalLanguageToken[] = ["py", "js", "rb", "jl"];
29
+ const EVAL_LANGUAGE_RUNTIME: Record<EvalLanguageToken, string> = {
30
+ py: '"py" for the IPython kernel',
31
+ js: '"js" for the persistent JS VM',
32
+ rb: '"rb" for the persistent Ruby kernel',
33
+ jl: '"jl" for the persistent Julia kernel',
34
+ };
35
+ const EVAL_LANGUAGE_NAME: Record<EvalLanguageToken, string> = {
36
+ py: "Python",
37
+ js: "JavaScript",
38
+ rb: "Ruby",
39
+ jl: "Julia",
40
+ };
41
+ const EVAL_CELLS_DESCRIPTION =
42
+ "cells executed in order. State persists within each language across cells and tool calls.";
43
+
44
+ /** Join names as an English "or" list: ["A"]→"A", ["A","B"]→"A or B", 3+→"A, B, or C". */
45
+ function joinWithOr(items: readonly string[]): string {
46
+ if (items.length <= 1) return items[0] ?? "";
47
+ if (items.length === 2) return `${items[0]} or ${items[1]}`;
48
+ return `${items.slice(0, -1).join(", ")}, or ${items[items.length - 1]}`;
49
+ }
50
+
51
+ function describeLanguageField(langs: readonly EvalLanguageToken[]): string {
52
+ return `runtime: ${langs.map(lang => EVAL_LANGUAGE_RUNTIME[lang]).join(", ")}`;
53
+ }
54
+
55
+ function describeCodeField(langs: readonly EvalLanguageToken[]): string {
56
+ const replLangs = langs.filter(lang => lang === "rb" || lang === "jl");
57
+ // No persistent REPL backends → keep the original py/js phrasing verbatim so the
58
+ // default (rb/jl off) wire schema stays byte-identical to the pre-feature one.
59
+ if (replLangs.length === 0) return "cell body, verbatim. Use top-level await freely.";
60
+ const awaitLangs = langs.filter(lang => lang === "py" || lang === "js");
61
+ const clauses: string[] = [];
62
+ if (awaitLangs.length > 0) clauses.push(`Top-level \`await\` is available in ${awaitLangs.join("/")}`);
63
+ clauses.push(`${replLangs.join("/")} auto-display the last expression like a REPL`);
64
+ return `cell body, verbatim. ${clauses.join("; ")}.`;
65
+ }
66
+
67
+ /** One-line discovery summary listing the runtimes available this session. */
68
+ function summarizeEvalLanguages(langs: readonly EvalLanguageToken[]): string {
69
+ const names = langs.map(lang => EVAL_LANGUAGE_NAME[lang]);
70
+ const list = names.length > 0 ? joinWithOr(names) : "Python or JavaScript";
71
+ // "in-process" matches the historical py/js summary; persistent kernels (rb/jl) switch wording.
72
+ const backend = langs.some(lang => lang === "rb" || lang === "jl") ? "a persistent" : "an in-process";
73
+ return `Execute ${list} code in ${backend} eval backend`;
74
+ }
75
+
76
+ /** Resolved-allowance → enabled language tokens, preserving display order. */
77
+ function enabledEvalLanguages(backends: EvalBackendsAllowance): EvalLanguageToken[] {
78
+ const allowed: Record<EvalLanguageToken, boolean> = {
79
+ py: backends.python,
80
+ js: backends.js,
81
+ rb: backends.ruby,
82
+ jl: backends.julia,
83
+ };
84
+ return EVAL_LANGUAGE_ORDER.filter(lang => allowed[lang]);
85
+ }
86
+
87
+ const evalCellCommonFields = {
33
88
  "title?": type("string").describe('short label shown in transcript (e.g. "imports", "load config")'),
34
89
  "timeout?": type("number").describe("per-cell timeout in seconds"),
35
90
  "reset?": type("boolean").describe(
36
91
  "wipe this cell's language kernel before running. Other languages are untouched.",
37
92
  ),
93
+ };
94
+
95
+ /**
96
+ * Per-cell input. Each cell runs in order; state persists within a language
97
+ * across cells and across tool calls. This static schema carries the full
98
+ * language union for typing; {@link buildEvalSchema} narrows the wire copy per
99
+ * session so disabled backends are never advertised to the model.
100
+ */
101
+ const evalCellSchema = type({
102
+ language: type("'py' | 'js' | 'rb' | 'jl'").describe(describeLanguageField(EVAL_LANGUAGE_ORDER)),
103
+ code: type("string").describe(describeCodeField(EVAL_LANGUAGE_ORDER)),
104
+ ...evalCellCommonFields,
38
105
  });
39
106
  export type EvalCellInput = typeof evalCellSchema.infer;
40
107
 
41
108
  export const evalSchema = type({
42
- cells: evalCellSchema
43
- .array()
44
- .atLeastLength(1)
45
- .describe("cells executed in order. State persists within each language across cells and tool calls."),
109
+ cells: evalCellSchema.array().atLeastLength(1).describe(EVAL_CELLS_DESCRIPTION),
46
110
  });
47
111
  export type EvalToolParams = typeof evalSchema.infer;
48
112
 
113
+ /**
114
+ * Build a session-scoped copy of the eval schema whose `language` enum and field
115
+ * descriptions advertise only the runtimes enabled for this session. Disabled
116
+ * backends never reach the model: the wire schema, BM25 discovery corpus, and
117
+ * tool description stay in lockstep with {@link resolveEvalBackends}. The static
118
+ * {@link evalSchema} (full union) remains the type-level source of truth.
119
+ */
120
+ function buildEvalSchema(langs: readonly EvalLanguageToken[]): typeof evalSchema {
121
+ const cellSchema = type({
122
+ language: type.enumerated(...langs).describe(describeLanguageField(langs)),
123
+ code: type("string").describe(describeCodeField(langs)),
124
+ ...evalCellCommonFields,
125
+ });
126
+ const schema = type({
127
+ cells: cellSchema.array().atLeastLength(1).describe(EVAL_CELLS_DESCRIPTION),
128
+ });
129
+ return schema as unknown as typeof evalSchema;
130
+ }
131
+
49
132
  export type EvalToolResult = {
50
133
  content: Array<{ type: "text"; text: string }>;
51
134
  details: EvalToolDetails | undefined;
@@ -88,6 +171,8 @@ function formatDisplayOutputsForText(outputs: EvalDisplayOutput[]): string {
88
171
  export interface EvalToolDescriptionOptions {
89
172
  py?: boolean;
90
173
  js?: boolean;
174
+ rb?: boolean;
175
+ jl?: boolean;
91
176
  /**
92
177
  * Whether `agent()` is allowed in this session. Driven by the parent's
93
178
  * spawn policy (`getSessionSpawns`). Defaults to `true` for backward
@@ -101,8 +186,10 @@ export interface EvalToolDescriptionOptions {
101
186
  export function getEvalToolDescription(options: EvalToolDescriptionOptions = {}): string {
102
187
  const py = options.py ?? true;
103
188
  const js = options.js ?? true;
189
+ const rb = options.rb ?? false;
190
+ const jl = options.jl ?? false;
104
191
  const spawns = options.spawns ?? true;
105
- return prompt.render(evalDescription, { py, js, spawns });
192
+ return prompt.render(evalDescription, { py, js, rb, jl, spawns });
106
193
  }
107
194
 
108
195
  export interface EvalToolOptions {
@@ -142,19 +229,61 @@ async function resolveBackend(session: ToolSession, language: EvalLanguage): Pro
142
229
  const backends = resolveEvalBackends(session);
143
230
  const allowPy = backends.python;
144
231
  const allowJs = backends.js;
232
+ const allowRb = backends.ruby;
233
+ const allowJl = backends.julia;
145
234
 
146
235
  if (language === "python") {
147
236
  if (!allowPy) throw new ToolError("Python backend is disabled (PI_PY=0 or eval.py = false).");
148
237
  if (!(await pythonBackend.isAvailable(session))) {
238
+ const alternatives = [allowJs ? '"js"' : null, allowRb ? '"rb"' : null, allowJl ? '"jl"' : null].filter(
239
+ Boolean,
240
+ );
149
241
  throw new ToolError(
150
- 'Python backend is unavailable in this session. Pass language: "js" or install the python kernel.',
242
+ alternatives.length > 0
243
+ ? `Python backend is unavailable in this session. Pass language: ${alternatives.join(" or ")} or install the python kernel.`
244
+ : 'Python backend is unavailable in this session. Install the python kernel to use language: "py".',
151
245
  );
152
246
  }
153
247
  return { backend: pythonBackend };
154
248
  }
249
+ if (language === "ruby") {
250
+ if (!allowRb) throw new ToolError("Ruby backend is disabled (PI_RB=0 or eval.rb = false).");
251
+ if (!(await rubyBackend.isAvailable(session))) {
252
+ const alternatives = [allowJs ? '"js"' : null, allowPy ? '"py"' : null, allowJl ? '"jl"' : null].filter(
253
+ Boolean,
254
+ );
255
+ throw new ToolError(
256
+ alternatives.length > 0
257
+ ? `Ruby backend is unavailable in this session. Pass language: ${alternatives.join(" or ")} or install Ruby.`
258
+ : 'Ruby backend is unavailable in this session. Install Ruby to use language: "rb".',
259
+ );
260
+ }
261
+ return { backend: rubyBackend };
262
+ }
263
+ if (language === "julia") {
264
+ if (!allowJl) throw new ToolError("Julia backend is disabled (PI_JL=0 or eval.jl = false).");
265
+ if (!(await juliaBackend.isAvailable(session))) {
266
+ const alternatives = [allowJs ? '"js"' : null, allowPy ? '"py"' : null, allowRb ? '"rb"' : null].filter(
267
+ Boolean,
268
+ );
269
+ throw new ToolError(
270
+ alternatives.length > 0
271
+ ? `Julia backend is unavailable in this session. Pass language: ${alternatives.join(" or ")} or install Julia.`
272
+ : 'Julia backend is unavailable in this session. Install Julia to use language: "jl".',
273
+ );
274
+ }
275
+ return { backend: juliaBackend };
276
+ }
155
277
  if (!allowJs) throw new ToolError("JavaScript backend is disabled (PI_JS=0 or eval.js = false).");
156
278
  return { backend: jsBackend };
157
279
  }
280
+ function formatEvalInputLanguage(value: string): string {
281
+ if (value === "py" || value === "python") return "python";
282
+ if (value === "js" || value === "javascript") return "javascript";
283
+ if (value === "rb" || value === "ruby") return "ruby";
284
+ if (value === "jl" || value === "julia") return "julia";
285
+ return value;
286
+ }
158
287
 
159
288
  export class EvalTool implements AgentTool<typeof evalSchema> {
160
289
  readonly name = "eval";
@@ -164,7 +293,8 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
164
293
  const cells = Array.isArray(params.cells) ? params.cells : [];
165
294
  const firstCell = cells[0] as Partial<EvalCellInput> | undefined;
166
295
  if (!firstCell) return [];
167
- const language = typeof firstCell.language === "string" ? firstCell.language : "(missing)";
296
+ const language =
297
+ typeof firstCell.language === "string" ? formatEvalInputLanguage(firstCell.language) : "javascript (default)";
168
298
  const code = typeof firstCell.code === "string" ? firstCell.code : "";
169
299
  const lines = [`Language: ${language}`, `Code:\n${truncateForPrompt(code)}`];
170
300
  if (cells.length > 1) {
@@ -172,7 +302,9 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
172
302
  }
173
303
  return lines;
174
304
  };
175
- readonly summary = "Execute Python or JavaScript code in an in-process eval backend";
305
+ get summary(): string {
306
+ return summarizeEvalLanguages(this.#enabledLanguages());
307
+ }
176
308
  readonly loadMode = "discoverable";
177
309
  readonly label = "Eval";
178
310
  get description(): string {
@@ -180,7 +312,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
180
312
  const backends = resolveEvalBackends(this.session);
181
313
  const sessionSpawns = this.session.getSessionSpawns?.() ?? "*";
182
314
  const spawnsAllowed = sessionSpawns !== "" && sessionSpawns !== null;
183
- return getEvalToolDescription({ py: backends.python, js: backends.js, spawns: spawnsAllowed });
315
+ return getEvalToolDescription({
316
+ py: backends.python,
317
+ js: backends.js,
318
+ rb: backends.ruby,
319
+ jl: backends.julia,
320
+ spawns: spawnsAllowed,
321
+ });
184
322
  }
185
323
  readonly examples: readonly ToolExample<typeof evalSchema.infer>[] = [
186
324
  {
@@ -201,7 +339,16 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
201
339
  },
202
340
  },
203
341
  ];
204
- readonly parameters = evalSchema;
342
+ get parameters(): typeof evalSchema {
343
+ const langs = this.#enabledLanguages();
344
+ if (langs.length === 0 || langs.length === EVAL_LANGUAGE_ORDER.length) return evalSchema;
345
+ const key = langs.join(",");
346
+ if (this.#paramsKey !== key) {
347
+ this.#cachedParams = buildEvalSchema(langs);
348
+ this.#paramsKey = key;
349
+ }
350
+ return this.#cachedParams ?? evalSchema;
351
+ }
205
352
  readonly concurrency = "exclusive";
206
353
  readonly strict = true;
207
354
  readonly intent = (args: Partial<typeof evalSchema.infer>): string | undefined => {
@@ -209,13 +356,24 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
209
356
  const first = cells.find(c => c && typeof c === "object");
210
357
  if (!first) return "evaluating";
211
358
  const title = typeof first.title === "string" ? first.title : undefined;
212
- const language = typeof first.language === "string" ? first.language : "?";
359
+ const language = typeof first.language === "string" ? formatEvalInputLanguage(first.language) : "javascript";
213
360
  const label = title || `running ${language}`;
214
361
  return cells.length > 1 ? `${label} (+${cells.length - 1})` : label;
215
362
  };
216
363
 
217
364
  readonly #proxyExecutor?: EvalProxyExecutor;
218
365
 
366
+ #paramsKey?: string;
367
+ #cachedParams?: typeof evalSchema;
368
+
369
+ /**
370
+ * Languages enabled for this session, in display order. Detached tools (no
371
+ * session) fall back to the shipped defaults (py/js; rb/jl are opt-in).
372
+ */
373
+ #enabledLanguages(): EvalLanguageToken[] {
374
+ return this.session ? enabledEvalLanguages(resolveEvalBackends(this.session)) : ["py", "js"];
375
+ }
376
+
219
377
  constructor(
220
378
  private readonly session: ToolSession | null,
221
379
  options?: EvalToolOptions,
@@ -243,7 +401,14 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
243
401
  const cells: ResolvedEvalCell[] = [];
244
402
  for (let i = 0; i < params.cells.length; i++) {
245
403
  const cell = params.cells[i];
246
- const language: EvalLanguage = cell.language === "py" ? "python" : "js";
404
+ const language: EvalLanguage =
405
+ cell.language === "py"
406
+ ? "python"
407
+ : cell.language === "rb"
408
+ ? "ruby"
409
+ : cell.language === "jl"
410
+ ? "julia"
411
+ : "js";
247
412
  const resolved = await resolveBackend(session, language);
248
413
  cells.push({
249
414
  index: i,
@@ -348,7 +513,7 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
348
513
  // The per-cell `timeout` is a budget on the cell runtime's *own*
349
514
  // work. Host-side `agent()`/`parallel()`/`completion()` bridge calls suspend
350
515
  // that budget entirely and restart a fresh timeout window when control
351
- // returns to Python/JS. Compute, stdout, `log()`/`phase()`, and
516
+ // returns to the active backend runtime. Compute, stdout, `log()`/`phase()`, and
352
517
  // ordinary tool calls all count against the budget. The watchdog drives
353
518
  // `combinedSignal`; we pass no wall-clock deadline downstream so the
354
519
  // backends never arm a competing fixed timer.
@@ -7,7 +7,9 @@ import type { Rule } from "../capability/rule";
7
7
  import type { PromptTemplate } from "../config/prompt-templates";
8
8
  import type { Settings } from "../config/settings";
9
9
  import { EditTool } from "../edit";
10
+ import { checkJuliaKernelAvailability } from "../eval/jl/kernel";
10
11
  import { checkPythonKernelAvailability } from "../eval/py/kernel";
12
+ import { checkRubyKernelAvailability } from "../eval/rb/kernel";
11
13
  import type { ToolPathWithSource } from "../extensibility/custom-tools";
12
14
  import type { Skill } from "../extensibility/skills";
13
15
  import type { GoalModeState, GoalRuntime } from "../goals";
@@ -165,7 +167,7 @@ export interface ToolSession {
165
167
  suppressSpawnAdvisory?: boolean;
166
168
  /** Optional fetch implementation injected into the URL read pipeline (tests, proxies). Defaults to global fetch. */
167
169
  fetch?: FetchImpl;
168
- /** Skip Python kernel availability check and warmup */
170
+ /** Skip subprocess-kernel availability checks and warmup */
169
171
  skipPythonPreflight?: boolean;
170
172
  /** Pre-loaded context files (AGENTS.md, etc) */
171
173
  contextFiles?: ContextFileEntry[];
@@ -202,13 +204,13 @@ export interface ToolSession {
202
204
  requireYieldTool?: boolean;
203
205
  /** Task recursion depth (0 = top-level, 1 = first child, etc.) */
204
206
  taskDepth?: number;
205
- /** Get shared eval executor session ID. Subagents inherit this to share JS/Python state. */
207
+ /** Get shared eval executor session ID. Subagents inherit this to share JS/Python/Ruby/Julia state. */
206
208
  getEvalSessionId?: () => string | null;
207
209
  /** Get session file */
208
210
  getSessionFile: () => string | null;
209
211
  /** Get eval kernel owner ID for session-scoped retained-kernel cleanup. */
210
212
  getEvalKernelOwnerId?: () => string | null;
211
- /** Reject new eval (python or js) work once session disposal has started. */
213
+ /** Reject new eval work once session disposal has started. */
212
214
  assertEvalExecutionAllowed?: () => void;
213
215
  /** Track tool-owned eval work so session disposal can await/abort it like direct session eval runs. */
214
216
  trackEvalExecution?<T>(execution: Promise<T>, abortController: AbortController): Promise<T>;
@@ -486,36 +488,57 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
486
488
  const backends = resolveEvalBackends(session);
487
489
  const allowPython = backends.python;
488
490
  const allowJs = backends.js;
489
- const skipPythonPreflight = session.skipPythonPreflight === true;
490
- // Eval tool is enabled if EITHER backend is reachable. We only need to know
491
- // whether python is reachable when JS is disabled — otherwise allowEval is
492
- // already true and the python-availability check can be deferred to first
493
- // invocation of the python backend (already handled inside the executor).
491
+ const allowRuby = backends.ruby;
492
+ const allowJulia = backends.julia;
493
+ const skipEvalPreflight = session.skipPythonPreflight === true;
494
+ // Eval tool is enabled if ANY backend is reachable. JS needs no preflight, so
495
+ // we only probe Python/Ruby/Julia when JS is disabled otherwise allowEval is
496
+ // already true and per-backend availability is checked at first invocation.
494
497
  let pythonAvailable = true;
495
- if (
496
- !skipPythonPreflight &&
497
- allowPython &&
498
- !allowJs &&
499
- (requestedTools === undefined || requestedTools.includes("eval"))
500
- ) {
501
- const availability = await logger.time(
502
- "createTools:pythonCheck",
503
- checkPythonKernelAvailability,
504
- session.cwd,
505
- session.settings.get("python.interpreter")?.trim() || undefined,
506
- );
507
- pythonAvailable = availability.ok;
508
- if (!availability.ok) {
509
- logger.warn("Python kernel unavailable and JS backend disabled; eval will be unavailable", {
510
- reason: availability.reason,
511
- });
498
+ let rubyAvailable = true;
499
+ let juliaAvailable = true;
500
+ const evalRequested = requestedTools === undefined || requestedTools.includes("eval");
501
+ if (!skipEvalPreflight && !allowJs && evalRequested) {
502
+ if (allowPython) {
503
+ const availability = await logger.time(
504
+ "createTools:pythonCheck",
505
+ checkPythonKernelAvailability,
506
+ session.cwd,
507
+ session.settings.get("python.interpreter")?.trim() || undefined,
508
+ );
509
+ pythonAvailable = availability.ok;
510
+ if (!availability.ok) {
511
+ logger.warn("Python kernel unavailable and JS backend disabled", { reason: availability.reason });
512
+ }
513
+ }
514
+ if (allowRuby) {
515
+ const availability = await checkRubyKernelAvailability(
516
+ session.cwd,
517
+ session.settings.get("ruby.interpreter")?.trim() || undefined,
518
+ );
519
+ rubyAvailable = availability.ok;
520
+ if (!availability.ok) {
521
+ logger.warn("Ruby kernel unavailable and JS backend disabled", { reason: availability.reason });
522
+ }
523
+ }
524
+ if (allowJulia) {
525
+ const availability = await checkJuliaKernelAvailability(
526
+ session.cwd,
527
+ session.settings.get("julia.interpreter")?.trim() || undefined,
528
+ );
529
+ juliaAvailable = availability.ok;
530
+ if (!availability.ok) {
531
+ logger.warn("Julia kernel unavailable and JS backend disabled", { reason: availability.reason });
532
+ }
512
533
  }
513
534
  }
514
535
 
515
536
  const effectivePythonAllowed = allowPython && pythonAvailable;
516
- // Eval is exposed whenever any backend is reachable. The python backend may
517
- // be unreachable, in which case eval dispatches exclusively to js.
518
- const allowEval = effectivePythonAllowed || allowJs;
537
+ const effectiveRubyAllowed = allowRuby && rubyAvailable;
538
+ const effectiveJuliaAllowed = allowJulia && juliaAvailable;
539
+ // Eval is exposed whenever any backend is reachable. A backend may be
540
+ // unreachable, in which case eval dispatches exclusively to the others.
541
+ const allowEval = effectivePythonAllowed || allowJs || effectiveRubyAllowed || effectiveJuliaAllowed;
519
542
 
520
543
  // Auto-include AST counterparts when their text-based sibling is present
521
544
  if (requestedTools) {