@gajae-code/coding-agent 0.5.4 → 0.6.1

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 (155) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/cli/web-search-cli.d.ts +12 -0
  3. package/dist/types/commands/rlm.d.ts +10 -0
  4. package/dist/types/commands/web-search.d.ts +54 -0
  5. package/dist/types/config/keybindings.d.ts +10 -0
  6. package/dist/types/config/model-profiles.d.ts +2 -1
  7. package/dist/types/config/model-registry.d.ts +3 -0
  8. package/dist/types/config/models-config-schema.d.ts +3 -0
  9. package/dist/types/config/settings-schema.d.ts +61 -3
  10. package/dist/types/edit/notebook.d.ts +3 -0
  11. package/dist/types/eval/py/executor.d.ts +3 -0
  12. package/dist/types/eval/py/kernel.d.ts +3 -1
  13. package/dist/types/eval/py/runtime.d.ts +9 -1
  14. package/dist/types/exec/bash-executor.d.ts +4 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  16. package/dist/types/extensibility/custom-tools/wrapper.d.ts +1 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  18. package/dist/types/extensibility/extensions/wrapper.d.ts +1 -0
  19. package/dist/types/gjc-runtime/launch-tmux.d.ts +6 -0
  20. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +14 -0
  21. package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
  22. package/dist/types/gjc-runtime/tmux-gc.d.ts +3 -3
  23. package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -0
  24. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +18 -0
  25. package/dist/types/goals/state.d.ts +1 -1
  26. package/dist/types/goals/tools/goal-tool.d.ts +2 -0
  27. package/dist/types/main.d.ts +11 -0
  28. package/dist/types/modes/components/custom-editor.d.ts +4 -2
  29. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +12 -0
  30. package/dist/types/modes/components/model-selector.d.ts +5 -2
  31. package/dist/types/modes/components/status-line.d.ts +4 -1
  32. package/dist/types/modes/controllers/input-controller.d.ts +3 -0
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  34. package/dist/types/modes/print-mode.d.ts +6 -0
  35. package/dist/types/modes/rpc/rpc-client.d.ts +21 -0
  36. package/dist/types/modes/rpc/rpc-socket-security.d.ts +7 -0
  37. package/dist/types/modes/rpc/rpc-types.d.ts +13 -0
  38. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +2 -0
  39. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +1 -0
  40. package/dist/types/rlm/artifacts.d.ts +9 -0
  41. package/dist/types/rlm/complete-research-tool.d.ts +35 -0
  42. package/dist/types/rlm/data-context.d.ts +6 -0
  43. package/dist/types/rlm/index.d.ts +35 -0
  44. package/dist/types/rlm/notebook.d.ts +12 -0
  45. package/dist/types/rlm/preset.d.ts +23 -0
  46. package/dist/types/rlm/python-tool.d.ts +16 -0
  47. package/dist/types/rlm/report.d.ts +14 -0
  48. package/dist/types/rlm/types.d.ts +37 -0
  49. package/dist/types/sdk.d.ts +7 -0
  50. package/dist/types/session/agent-session.d.ts +21 -0
  51. package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -1
  52. package/dist/types/tools/browser/attach.d.ts +19 -3
  53. package/dist/types/tools/browser/registry.d.ts +15 -0
  54. package/dist/types/tools/browser/render.d.ts +3 -0
  55. package/dist/types/tools/browser.d.ts +18 -1
  56. package/dist/types/tools/computer/render.d.ts +17 -0
  57. package/dist/types/tools/computer.d.ts +465 -0
  58. package/dist/types/tools/index.d.ts +24 -1
  59. package/dist/types/tools/job.d.ts +13 -0
  60. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  61. package/dist/types/web/search/index.d.ts +32 -2
  62. package/dist/types/web/search/providers/base.d.ts +22 -0
  63. package/dist/types/web/search/providers/xai.d.ts +64 -0
  64. package/dist/types/web/search/types.d.ts +11 -3
  65. package/package.json +7 -7
  66. package/src/cli/web-search-cli.ts +123 -8
  67. package/src/cli.ts +2 -0
  68. package/src/commands/rlm.ts +19 -0
  69. package/src/commands/web-search.ts +66 -0
  70. package/src/config/keybindings.ts +11 -0
  71. package/src/config/model-profiles.ts +11 -3
  72. package/src/config/model-registry.ts +55 -1
  73. package/src/config/models-config-schema.ts +1 -0
  74. package/src/config/settings-schema.ts +67 -1
  75. package/src/edit/notebook.ts +6 -2
  76. package/src/eval/py/executor.ts +8 -1
  77. package/src/eval/py/kernel.ts +9 -4
  78. package/src/eval/py/runtime.ts +153 -32
  79. package/src/exec/bash-executor.ts +10 -4
  80. package/src/extensibility/custom-tools/types.ts +2 -0
  81. package/src/extensibility/custom-tools/wrapper.ts +2 -0
  82. package/src/extensibility/extensions/types.ts +2 -0
  83. package/src/extensibility/extensions/wrapper.ts +1 -0
  84. package/src/gjc-runtime/launch-tmux.ts +129 -1
  85. package/src/gjc-runtime/session-state-sidecar.ts +61 -1
  86. package/src/gjc-runtime/tmux-common.ts +26 -2
  87. package/src/gjc-runtime/tmux-gc.ts +40 -27
  88. package/src/gjc-runtime/tmux-sessions.ts +13 -1
  89. package/src/gjc-runtime/ultragoal-runtime.ts +340 -18
  90. package/src/goals/runtime.ts +4 -3
  91. package/src/goals/state.ts +1 -1
  92. package/src/goals/tools/goal-tool.ts +16 -3
  93. package/src/internal-urls/docs-index.generated.ts +13 -9
  94. package/src/main.ts +28 -3
  95. package/src/modes/components/custom-editor.ts +13 -4
  96. package/src/modes/components/custom-model-preset-wizard.ts +293 -0
  97. package/src/modes/components/hook-selector.ts +1 -1
  98. package/src/modes/components/model-selector.ts +72 -29
  99. package/src/modes/components/skill-message.ts +62 -8
  100. package/src/modes/components/status-line.ts +13 -1
  101. package/src/modes/controllers/input-controller.ts +60 -11
  102. package/src/modes/controllers/selector-controller.ts +39 -0
  103. package/src/modes/interactive-mode.ts +1 -1
  104. package/src/modes/print-mode.ts +14 -4
  105. package/src/modes/rpc/rpc-client.ts +250 -80
  106. package/src/modes/rpc/rpc-mode.ts +6 -12
  107. package/src/modes/rpc/rpc-socket-security.ts +103 -0
  108. package/src/modes/rpc/rpc-types.ts +10 -0
  109. package/src/modes/shared/agent-wire/command-dispatch.ts +7 -0
  110. package/src/modes/shared/agent-wire/command-validation.ts +1 -0
  111. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  112. package/src/modes/shared/agent-wire/unattended-session.ts +9 -0
  113. package/src/modes/utils/hotkeys-markdown.ts +4 -2
  114. package/src/modes/utils/ui-helpers.ts +2 -2
  115. package/src/prompts/goals/goal-continuation.md +1 -0
  116. package/src/prompts/goals/goal-mode-active.md +1 -0
  117. package/src/prompts/system/rlm-report-command.md +1 -0
  118. package/src/prompts/system/rlm-research.md +23 -0
  119. package/src/prompts/tools/bash.md +23 -2
  120. package/src/prompts/tools/browser.md +7 -3
  121. package/src/prompts/tools/computer.md +74 -0
  122. package/src/prompts/tools/goal.md +3 -0
  123. package/src/prompts/tools/job.md +9 -1
  124. package/src/prompts/tools/web-search.md +7 -0
  125. package/src/rlm/artifacts.ts +60 -0
  126. package/src/rlm/complete-research-tool.ts +163 -0
  127. package/src/rlm/data-context.ts +26 -0
  128. package/src/rlm/index.ts +339 -0
  129. package/src/rlm/notebook.ts +108 -0
  130. package/src/rlm/preset.ts +76 -0
  131. package/src/rlm/python-tool.ts +68 -0
  132. package/src/rlm/report.ts +70 -0
  133. package/src/rlm/types.ts +40 -0
  134. package/src/sdk.ts +12 -0
  135. package/src/session/agent-session.ts +48 -3
  136. package/src/slash-commands/builtin-registry.ts +17 -0
  137. package/src/tools/bash-allowed-prefixes.ts +84 -1
  138. package/src/tools/bash.ts +80 -13
  139. package/src/tools/browser/attach.ts +103 -3
  140. package/src/tools/browser/registry.ts +176 -2
  141. package/src/tools/browser/render.ts +9 -1
  142. package/src/tools/browser.ts +33 -0
  143. package/src/tools/computer/render.ts +78 -0
  144. package/src/tools/computer.ts +640 -0
  145. package/src/tools/index.ts +41 -1
  146. package/src/tools/job.ts +88 -5
  147. package/src/tools/json-tree.ts +42 -29
  148. package/src/tools/renderers.ts +2 -0
  149. package/src/tools/tool-timeouts.ts +1 -0
  150. package/src/web/search/index.ts +27 -2
  151. package/src/web/search/provider.ts +16 -1
  152. package/src/web/search/providers/base.ts +22 -0
  153. package/src/web/search/providers/xai.ts +511 -0
  154. package/src/web/search/render.ts +7 -0
  155. package/src/web/search/types.ts +11 -1
@@ -1000,7 +1000,7 @@ export const SETTINGS_SCHEMA = {
1000
1000
  tab: "interaction",
1001
1001
  label: "Busy Prompt Mode",
1002
1002
  description:
1003
- "What a submitted prompt does while the agent is busy: steer (interrupt the active turn) or queue (run after the active turn completes)",
1003
+ "What a submitted prompt does while the agent is busy: queue normal chat for the next turn, or steer to interrupt the active turn",
1004
1004
  },
1005
1005
  },
1006
1006
 
@@ -2160,6 +2160,66 @@ export const SETTINGS_SCHEMA = {
2160
2160
  },
2161
2161
  },
2162
2162
 
2163
+ "computer.enabled": {
2164
+ type: "boolean",
2165
+ default: false,
2166
+ ui: {
2167
+ tab: "tools",
2168
+ label: "Computer",
2169
+ description: "Enable the macOS computer tool for this session. Off by default.",
2170
+ },
2171
+ },
2172
+
2173
+ "computer.alwaysOn": {
2174
+ type: "boolean",
2175
+ default: false,
2176
+ ui: {
2177
+ tab: "tools",
2178
+ label: "Computer Always On",
2179
+ description: "Keep the macOS computer tool callable without per-session enablement.",
2180
+ },
2181
+ },
2182
+
2183
+ "computer.autoScreenshot": {
2184
+ type: "boolean",
2185
+ default: false,
2186
+ ui: {
2187
+ tab: "tools",
2188
+ label: "Computer Auto Screenshot",
2189
+ description: "Automatically request bounded screenshots after computer actions when supported.",
2190
+ },
2191
+ },
2192
+
2193
+ "computer.screenshotMaxBytes": {
2194
+ type: "number",
2195
+ default: 5_000_000,
2196
+ ui: {
2197
+ tab: "tools",
2198
+ label: "Computer Screenshot Max Bytes",
2199
+ description: "Maximum screenshot payload size for computer action results.",
2200
+ },
2201
+ },
2202
+
2203
+ "computer.killSwitchHotkey": {
2204
+ type: "string",
2205
+ default: "Control+Option+Command+Escape",
2206
+ ui: {
2207
+ tab: "tools",
2208
+ label: "Computer Kill Switch Hotkey",
2209
+ description: "Native stop/suspend hotkey shown to users for computer-use sessions.",
2210
+ },
2211
+ },
2212
+
2213
+ "computer.auditLog.enabled": {
2214
+ type: "boolean",
2215
+ default: true,
2216
+ ui: {
2217
+ tab: "tools",
2218
+ label: "Computer Audit Log",
2219
+ description: "Persist audit records for enabled computer-use actions.",
2220
+ },
2221
+ },
2222
+
2163
2223
  // Tool execution
2164
2224
  "tools.intentTracing": {
2165
2225
  type: "boolean",
@@ -2647,6 +2707,7 @@ export const SETTINGS_SCHEMA = {
2647
2707
  "anthropic",
2648
2708
  "gemini",
2649
2709
  "codex",
2710
+ "xai",
2650
2711
  "tavily",
2651
2712
  "kagi",
2652
2713
  "synthetic",
@@ -2688,6 +2749,11 @@ export const SETTINGS_SCHEMA = {
2688
2749
  label: "OpenAI",
2689
2750
  description: "OpenAI's native web_search (uses ChatGPT OAuth via /login openai-codex)",
2690
2751
  },
2752
+ {
2753
+ value: "xai",
2754
+ label: "xAI",
2755
+ description: "xAI Responses web_search/x_search (uses xAI OAuth via /login xai or XAI_API_KEY)",
2756
+ },
2691
2757
  {
2692
2758
  value: "gemini",
2693
2759
  label: "Gemini",
@@ -49,7 +49,7 @@ function cloneCell(cell: NotebookCell): NotebookCell {
49
49
  return structuredClone(cell);
50
50
  }
51
51
 
52
- function createNotebookCell(cellType: NotebookCellType, source: string): NotebookCell {
52
+ export function createNotebookCell(cellType: NotebookCellType, source: string): NotebookCell {
53
53
  const cell: NotebookCell = {
54
54
  cell_type: cellType,
55
55
  metadata: {},
@@ -62,7 +62,7 @@ function createNotebookCell(cellType: NotebookCellType, source: string): Noteboo
62
62
  return cell;
63
63
  }
64
64
 
65
- function createEmptyNotebook(): NotebookDocument {
65
+ export function createEmptyNotebook(): NotebookDocument {
66
66
  return {
67
67
  cells: [],
68
68
  metadata: {},
@@ -71,6 +71,10 @@ function createEmptyNotebook(): NotebookDocument {
71
71
  };
72
72
  }
73
73
 
74
+ export function serializeNotebookDocument(notebook: NotebookDocument): string {
75
+ return JSON.stringify(notebook, null, 1);
76
+ }
77
+
74
78
  function validateNotebook(value: unknown, displayPath: string): NotebookDocument {
75
79
  if (!isRecord(value)) {
76
80
  throw new Error(`Invalid notebook structure (expected object): ${displayPath}`);
@@ -12,6 +12,7 @@ import {
12
12
  type KernelExecuteResult,
13
13
  PythonKernel,
14
14
  } from "./kernel";
15
+ import type { PythonRuntimeOptions } from "./runtime";
15
16
  import { ensurePyToolBridge, registerPyToolBridge } from "./tool-bridge";
16
17
 
17
18
  export type PythonKernelMode = "session" | "per-call";
@@ -44,6 +45,8 @@ export interface PythonExecutorOptions {
44
45
  * preferred over `PI_SESSION_FILE`-derived paths.
45
46
  */
46
47
  artifactsDir?: string;
48
+ /** Runtime resolution/provisioning options (RLM managed workspace venv, package seeding). */
49
+ runtimeOptions?: PythonRuntimeOptions;
47
50
  /** Artifact path/id for full output storage */
48
51
  artifactPath?: string;
49
52
  artifactId?: string;
@@ -276,6 +279,7 @@ async function startKernel(cwd: string, options: PythonExecutorOptions): Promise
276
279
  return await PythonKernel.start({
277
280
  cwd,
278
281
  env: buildKernelEnv(options),
282
+ runtimeOptions: options.runtimeOptions,
279
283
  signal: options.signal,
280
284
  deadlineMs: options.deadlineMs,
281
285
  });
@@ -562,7 +566,10 @@ async function executeWithKernel(
562
566
  }
563
567
 
564
568
  async function ensureKernelAvailable(cwd: string, options: PythonExecutorOptions): Promise<void> {
565
- const availability = await waitForPromiseWithCancellation(checkPythonKernelAvailability(cwd), options);
569
+ const availability = await waitForPromiseWithCancellation(
570
+ checkPythonKernelAvailability(cwd, options.runtimeOptions),
571
+ options,
572
+ );
566
573
  if (!availability.ok) {
567
574
  throw new Error(availability.reason ?? "Python kernel unavailable");
568
575
  }
@@ -18,7 +18,7 @@ import { Settings } from "../../config/settings";
18
18
  import { type KernelDisplayOutput, renderKernelDisplay } from "./display";
19
19
  import { PYTHON_PRELUDE } from "./prelude";
20
20
  import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
21
- import { filterEnv, resolvePythonRuntime } from "./runtime";
21
+ import { ensurePythonRuntime, filterEnv, type PythonRuntimeOptions } from "./runtime";
22
22
 
23
23
  export type { KernelDisplayOutput, PythonStatusEvent } from "./display";
24
24
  export { renderKernelDisplay } from "./display";
@@ -89,6 +89,7 @@ interface KernelLifecycleOptions {
89
89
  interface KernelStartOptions extends KernelLifecycleOptions {
90
90
  cwd: string;
91
91
  env?: Record<string, string | undefined>;
92
+ runtimeOptions?: PythonRuntimeOptions;
92
93
  }
93
94
 
94
95
  interface KernelShutdownOptions {
@@ -120,7 +121,10 @@ function throwIfAborted(signal: AbortSignal | undefined, fallbackReason: string)
120
121
  throw createAbortError("AbortError", typeof reason === "string" ? reason : fallbackReason);
121
122
  }
122
123
 
123
- export async function checkPythonKernelAvailability(cwd: string): Promise<PythonKernelAvailability> {
124
+ export async function checkPythonKernelAvailability(
125
+ cwd: string,
126
+ runtimeOptions?: PythonRuntimeOptions,
127
+ ): Promise<PythonKernelAvailability> {
124
128
  if (isBunTestRuntime() || $flag("PI_PYTHON_SKIP_CHECK")) {
125
129
  return { ok: true };
126
130
  }
@@ -128,7 +132,7 @@ export async function checkPythonKernelAvailability(cwd: string): Promise<Python
128
132
  const settings = await Settings.init();
129
133
  const { env } = settings.getShellConfig();
130
134
  const baseEnv = filterEnv(env);
131
- const runtime = resolvePythonRuntime(cwd, baseEnv);
135
+ const runtime = await ensurePythonRuntime(cwd, baseEnv, runtimeOptions);
132
136
  const probe = await $`${runtime.pythonPath} -c "import sys;sys.exit(0)"`
133
137
  .quiet()
134
138
  .nothrow()
@@ -199,6 +203,7 @@ export class PythonKernel {
199
203
  "PythonKernel.start:availabilityCheck",
200
204
  checkPythonKernelAvailability,
201
205
  options.cwd,
206
+ options.runtimeOptions,
202
207
  );
203
208
  if (!availability.ok) {
204
209
  throw new Error(availability.reason ?? "Python kernel unavailable");
@@ -207,7 +212,7 @@ export class PythonKernel {
207
212
  const settings = await Settings.init();
208
213
  const { env: shellEnv } = settings.getShellConfig();
209
214
  const baseEnv = filterEnv(shellEnv);
210
- const runtime = resolvePythonRuntime(options.cwd, baseEnv);
215
+ const runtime = await ensurePythonRuntime(options.cwd, baseEnv, options.runtimeOptions);
211
216
  const spawnEnv: Record<string, string> = {};
212
217
  for (const [key, value] of Object.entries(runtime.env)) {
213
218
  if (typeof value === "string") spawnEnv[key] = value;
@@ -1,13 +1,23 @@
1
1
  /**
2
2
  * Python runtime resolution utilities.
3
3
  *
4
- * Centralizes environment filtering, venv detection, and Python executable resolution
5
- * for both the shared gateway and local kernel spawning.
4
+ * Centralizes environment filtering, venv detection, managed workspace venv
5
+ * provisioning, and Python executable resolution for both the shared gateway
6
+ * and local kernel spawning.
6
7
  */
7
8
  import * as fs from "node:fs";
8
9
  import * as path from "node:path";
9
10
  import { $env, $which, getPythonEnvDir } from "@gajae-code/utils";
10
11
 
12
+ export const RLM_MANAGED_PYTHON_PACKAGES: readonly string[] = ["numpy", "pandas", "matplotlib", "polars"];
13
+
14
+ export interface PythonRuntimeOptions {
15
+ /** Create/use <cwd>/.gjc/python-env when no BYO venv/conda env is present. */
16
+ managedWorkspaceVenv?: boolean;
17
+ /** Packages to seed into the managed workspace venv when provisioning it. */
18
+ seedPackages?: readonly string[];
19
+ }
20
+
11
21
  const DEFAULT_ENV_ALLOWLIST = new Set([
12
22
  "PATH",
13
23
  "HOME",
@@ -104,15 +114,22 @@ function resolvePathKey(env: Record<string, string | undefined>): string {
104
114
  return match ?? "PATH";
105
115
  }
106
116
 
107
- function resolveManagedPythonEnv(): string {
117
+ function resolveGlobalManagedPythonEnv(): string {
108
118
  return getPythonEnvDir();
109
119
  }
110
120
 
111
- function resolveManagedPythonCandidate(): { venvPath: string; pythonPath: string } {
112
- const venvPath = resolveManagedPythonEnv();
121
+ function resolvePythonCandidateInVenv(venvPath: string): { venvPath: string; pythonPath: string; binDir: string } {
113
122
  const binDir = process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
114
123
  const pythonPath = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
115
- return { venvPath, pythonPath };
124
+ return { venvPath, pythonPath, binDir };
125
+ }
126
+
127
+ function resolveManagedPythonCandidate(): { venvPath: string; pythonPath: string; binDir: string } {
128
+ return resolvePythonCandidateInVenv(resolveGlobalManagedPythonEnv());
129
+ }
130
+
131
+ function resolveWorkspaceManagedPythonCandidate(cwd: string): { venvPath: string; pythonPath: string; binDir: string } {
132
+ return resolvePythonCandidateInVenv(path.join(cwd, ".gjc", "python-env"));
116
133
  }
117
134
 
118
135
  export interface PythonRuntime {
@@ -124,6 +141,23 @@ export interface PythonRuntime {
124
141
  venvPath?: string;
125
142
  }
126
143
 
144
+ function runtimeFromVenv(
145
+ venvPath: string,
146
+ pythonPath: string,
147
+ binDir: string,
148
+ env: Record<string, string | undefined>,
149
+ ): PythonRuntime {
150
+ env.VIRTUAL_ENV = venvPath;
151
+ const pathKey = resolvePathKey(env);
152
+ const currentPath = env[pathKey];
153
+ env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
154
+ return {
155
+ pythonPath,
156
+ env,
157
+ venvPath,
158
+ };
159
+ }
160
+
127
161
  /**
128
162
  * Filter environment variables to a safe allowlist for Python subprocesses.
129
163
  * Removes sensitive API keys and limits to known-safe variables.
@@ -161,42 +195,110 @@ export function resolveVenvPath(cwd: string): string | undefined {
161
195
  return undefined;
162
196
  }
163
197
 
198
+ async function runRuntimeCommand(
199
+ cmd: string[],
200
+ cwd: string,
201
+ env: Record<string, string | undefined>,
202
+ description: string,
203
+ ): Promise<void> {
204
+ const spawnEnv: Record<string, string> = {};
205
+ for (const [key, value] of Object.entries(env)) {
206
+ if (typeof value === "string") spawnEnv[key] = value;
207
+ }
208
+ const proc = Bun.spawn(cmd, {
209
+ cwd,
210
+ env: spawnEnv,
211
+ stdout: "pipe",
212
+ stderr: "pipe",
213
+ windowsHide: true,
214
+ });
215
+ const [stdout, stderr, exitCode] = await Promise.all([
216
+ new Response(proc.stdout).text(),
217
+ new Response(proc.stderr).text(),
218
+ proc.exited,
219
+ ]);
220
+ if (exitCode !== 0) {
221
+ const output = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
222
+ throw new Error(`${description} failed with exit code ${exitCode}${output ? `: ${output}` : ""}`);
223
+ }
224
+ }
225
+
226
+ async function ensureWorkspaceManagedVenv(
227
+ cwd: string,
228
+ baseEnv: Record<string, string | undefined>,
229
+ seedPackages: readonly string[],
230
+ ): Promise<void> {
231
+ const managed = resolveWorkspaceManagedPythonCandidate(cwd);
232
+ if (!fs.existsSync(managed.pythonPath)) {
233
+ const basePython = $which("python3") ?? $which("python");
234
+ if (!basePython) throw new Error("Python executable not found on PATH");
235
+ await fs.promises.mkdir(path.dirname(managed.venvPath), { recursive: true });
236
+ await runRuntimeCommand(
237
+ [basePython, "-m", "venv", managed.venvPath],
238
+ cwd,
239
+ baseEnv,
240
+ "Managed Python venv creation",
241
+ );
242
+ }
243
+ if (seedPackages.length === 0) return;
244
+ const markerPath = path.join(managed.venvPath, ".gjc-seeded.json");
245
+ let seeded = false;
246
+ try {
247
+ const marker = JSON.parse(await fs.promises.readFile(markerPath, "utf8")) as { packages?: unknown };
248
+ const packages = Array.isArray(marker.packages) ? marker.packages : [];
249
+ seeded = seedPackages.every(pkg => packages.includes(pkg));
250
+ } catch {
251
+ seeded = false;
252
+ }
253
+ if (seeded) return;
254
+ const runtimeEnv = runtimeFromVenv(managed.venvPath, managed.pythonPath, managed.binDir, { ...baseEnv }).env;
255
+ await runRuntimeCommand(
256
+ [managed.pythonPath, "-m", "pip", "install", "--upgrade", "pip"],
257
+ cwd,
258
+ runtimeEnv,
259
+ "Managed Python pip bootstrap",
260
+ );
261
+ await runRuntimeCommand(
262
+ [managed.pythonPath, "-m", "pip", "install", ...seedPackages],
263
+ cwd,
264
+ runtimeEnv,
265
+ "Managed Python package seed",
266
+ );
267
+ await fs.promises.writeFile(
268
+ markerPath,
269
+ `${JSON.stringify({ packages: seedPackages, seededAt: new Date().toISOString() }, null, 2)}\n`,
270
+ "utf8",
271
+ );
272
+ }
273
+
164
274
  /**
165
275
  * Resolve Python runtime including executable path, environment, and venv detection.
166
276
  */
167
- export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string | undefined>): PythonRuntime {
277
+ export function resolvePythonRuntime(
278
+ cwd: string,
279
+ baseEnv: Record<string, string | undefined>,
280
+ options: PythonRuntimeOptions = {},
281
+ ): PythonRuntime {
168
282
  const env = { ...baseEnv };
169
283
  const venvPath = env.VIRTUAL_ENV ?? resolveVenvPath(cwd);
170
284
 
171
285
  if (venvPath) {
172
- env.VIRTUAL_ENV = venvPath;
173
- const binDir = process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
174
- const pythonCandidate = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
175
- if (fs.existsSync(pythonCandidate)) {
176
- const pathKey = resolvePathKey(env);
177
- const currentPath = env[pathKey];
178
- env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
179
- return {
180
- pythonPath: pythonCandidate,
181
- env,
182
- venvPath,
183
- };
286
+ const candidate = resolvePythonCandidateInVenv(venvPath);
287
+ if (fs.existsSync(candidate.pythonPath)) {
288
+ return runtimeFromVenv(candidate.venvPath, candidate.pythonPath, candidate.binDir, env);
184
289
  }
185
290
  }
186
291
 
187
- const managed = resolveManagedPythonCandidate();
188
- if (fs.existsSync(managed.pythonPath)) {
189
- env.VIRTUAL_ENV = managed.venvPath;
190
- const pathKey = resolvePathKey(env);
191
- const currentPath = env[pathKey];
192
- const managedBin =
193
- process.platform === "win32" ? path.join(managed.venvPath, "Scripts") : path.join(managed.venvPath, "bin");
194
- env[pathKey] = currentPath ? `${managedBin}${path.delimiter}${currentPath}` : managedBin;
195
- return {
196
- pythonPath: managed.pythonPath,
197
- env,
198
- venvPath: managed.venvPath,
199
- };
292
+ if (options.managedWorkspaceVenv) {
293
+ const workspaceManaged = resolveWorkspaceManagedPythonCandidate(cwd);
294
+ if (fs.existsSync(workspaceManaged.pythonPath)) {
295
+ return runtimeFromVenv(workspaceManaged.venvPath, workspaceManaged.pythonPath, workspaceManaged.binDir, env);
296
+ }
297
+ } else {
298
+ const managed = resolveManagedPythonCandidate();
299
+ if (fs.existsSync(managed.pythonPath)) {
300
+ return runtimeFromVenv(managed.venvPath, managed.pythonPath, managed.binDir, env);
301
+ }
200
302
  }
201
303
 
202
304
  const pythonPath = $which("python") ?? $which("python3");
@@ -208,3 +310,22 @@ export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
208
310
  env,
209
311
  };
210
312
  }
313
+
314
+ export async function ensurePythonRuntime(
315
+ cwd: string,
316
+ baseEnv: Record<string, string | undefined>,
317
+ options: PythonRuntimeOptions = {},
318
+ ): Promise<PythonRuntime> {
319
+ if (options.managedWorkspaceVenv) {
320
+ const env = { ...baseEnv };
321
+ const venvPath = env.VIRTUAL_ENV ?? resolveVenvPath(cwd);
322
+ if (venvPath) {
323
+ const candidate = resolvePythonCandidateInVenv(venvPath);
324
+ if (fs.existsSync(candidate.pythonPath)) {
325
+ return runtimeFromVenv(candidate.venvPath, candidate.pythonPath, candidate.binDir, env);
326
+ }
327
+ }
328
+ await ensureWorkspaceManagedVenv(cwd, baseEnv, options.seedPackages ?? RLM_MANAGED_PYTHON_PACKAGES);
329
+ }
330
+ return resolvePythonRuntime(cwd, baseEnv, options);
331
+ }
@@ -34,6 +34,10 @@ export interface BashExecutorOptions {
34
34
  artifactId?: string;
35
35
  /** Execute without retaining a native Shell in the persistent session registry. */
36
36
  oneShot?: boolean;
37
+ /** Ignore user-configured shell command prefixes. Used by constrained read-only shells. */
38
+ ignoreShellPrefix?: boolean;
39
+ /** Skip sourced shell snapshots. Used by constrained read-only shells. */
40
+ disableShellSnapshot?: boolean;
37
41
  /**
38
42
  * Invoked when the native minimizer rewrote the command's output, giving
39
43
  * the caller a chance to persist the lossless original capture (typically
@@ -119,15 +123,17 @@ export function buildMinimizerOptions(group: ShellMinimizerSettings): MinimizerO
119
123
  export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
120
124
  const settings = await Settings.init();
121
125
  const { shell, env: shellEnv, prefix } = settings.getShellConfig();
122
- const snapshotPath = shell.includes("bash") ? await getOrCreateSnapshot(shell, shellEnv) : null;
126
+ const configuredPrefix = options?.ignoreShellPrefix ? undefined : prefix;
127
+ const snapshotPath =
128
+ !options?.disableShellSnapshot && shell.includes("bash") ? await getOrCreateSnapshot(shell, shellEnv) : null;
123
129
 
124
130
  const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer"));
125
131
 
126
132
  const commandCwd = await resolveShellCwd(options?.cwd);
127
133
  const commandEnv = options?.env ? { ...NON_INTERACTIVE_ENV, ...options.env } : NON_INTERACTIVE_ENV;
128
134
 
129
- // Apply command prefix if configured
130
- const prefixedCommand = prefix ? `${prefix} ${command}` : command;
135
+ // Apply command prefix if configured and allowed for this execution.
136
+ const prefixedCommand = configuredPrefix ? `${configuredPrefix} ${command}` : command;
131
137
  const finalCommand = prefixedCommand;
132
138
 
133
139
  // Create output sink for truncation and artifact handling
@@ -160,7 +166,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
160
166
  }
161
167
 
162
168
  const usePersistentShell = options?.oneShot !== true;
163
- const sessionKey = buildSessionKey(shell, prefix, snapshotPath, shellEnv, options?.sessionKey, minimizer);
169
+ const sessionKey = buildSessionKey(shell, configuredPrefix, snapshotPath, shellEnv, options?.sessionKey, minimizer);
164
170
  const persistentSessionBroken = usePersistentShell && brokenShellSessions.has(sessionKey);
165
171
 
166
172
  let shellSession = persistentSessionBroken || !usePersistentShell ? undefined : shellSessions.get(sessionKey);
@@ -180,6 +180,8 @@ export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
180
180
  label: string;
181
181
  /** If true, tool is strictly typed and validated against the parameters schema before execution */
182
182
  strict?: boolean;
183
+ /** Tool scheduling mode; exclusive tools do not run in parallel with other tools in a model turn. */
184
+ concurrency?: "shared" | "exclusive";
183
185
  /** Description for LLM */
184
186
  description: string;
185
187
  /** Parameter schema (Zod or TypeBox; TypeBox is auto-lifted to Zod at registration). */
@@ -15,6 +15,7 @@ export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any
15
15
  declare description: string;
16
16
  declare parameters: TParams;
17
17
  readonly strict: boolean | undefined;
18
+ readonly concurrency: "shared" | "exclusive" | undefined;
18
19
 
19
20
  constructor(
20
21
  private tool: CustomTool<TParams, TDetails>,
@@ -22,6 +23,7 @@ export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any
22
23
  ) {
23
24
  applyToolProxy(tool, this);
24
25
  this.strict = tool.strict;
26
+ this.concurrency = tool.concurrency;
25
27
  }
26
28
 
27
29
  execute(
@@ -383,6 +383,8 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
383
383
  description: string;
384
384
  /** Parameter schema (Zod, or TypeBox for legacy/extension compat). */
385
385
  parameters: TParams;
386
+ /** Tool scheduling mode; exclusive tools do not run in parallel with other tools in a model turn. */
387
+ concurrency?: "shared" | "exclusive";
386
388
  /** If true, tool is excluded unless explicitly listed in --tools or agent's tools field */
387
389
  hidden?: boolean;
388
390
  /** If true, tool is registered but not auto-included in the initial active set.
@@ -17,6 +17,7 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
17
17
  declare parameters: any;
18
18
  declare label: string;
19
19
  declare strict: boolean;
20
+ declare concurrency: "shared" | "exclusive" | undefined;
20
21
 
21
22
  renderCall?: (args: any, options: any, theme: any) => any;
22
23
  renderResult?: (result: any, options: any, theme: any, args?: any) => any;