@gajae-code/coding-agent 0.18.0 → 0.18.5

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 (86) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/dist/types/cli/stats-cli.d.ts +0 -5
  3. package/dist/types/config/autorouting-tier-map.d.ts +29 -2
  4. package/dist/types/config/model-registry.d.ts +16 -1
  5. package/dist/types/config/settings-schema.d.ts +2 -2
  6. package/dist/types/exa/mcp-client.d.ts +1 -5
  7. package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
  8. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +1 -0
  9. package/dist/types/hashline/parser.d.ts +28 -5
  10. package/dist/types/modes/acp/acp-agent.d.ts +6 -1
  11. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  12. package/dist/types/sdk/broker/lifecycle.d.ts +6 -1
  13. package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
  14. package/dist/types/sdk/broker/runtime.d.ts +20 -0
  15. package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
  16. package/dist/types/sdk/bus/index.d.ts +1 -0
  17. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  18. package/dist/types/sdk/client/client.d.ts +5 -1
  19. package/dist/types/sdk/host/session-runtime.d.ts +7 -0
  20. package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
  21. package/dist/types/session/agent-session.d.ts +2 -0
  22. package/dist/types/task/index.d.ts +0 -1
  23. package/dist/types/tools/bash.d.ts +1 -0
  24. package/dist/types/tools/descriptors.d.ts +5 -0
  25. package/dist/types/tools/eval-backends.d.ts +37 -0
  26. package/dist/types/tools/eval.d.ts +1 -6
  27. package/dist/types/tools/index.d.ts +1 -36
  28. package/dist/types/tools/session-descriptions.d.ts +22 -0
  29. package/package.json +7 -7
  30. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  31. package/src/cli/doctor/managed-link.ts +4 -4
  32. package/src/cli/doctor/repairs.ts +9 -1
  33. package/src/cli/stats-cli.ts +4 -36
  34. package/src/cli-main.ts +4 -8
  35. package/src/cli.ts +4 -8
  36. package/src/commands/commit.ts +2 -2
  37. package/src/commands/sdk.ts +1 -2
  38. package/src/config/autorouting-tier-map.ts +20 -2
  39. package/src/config/model-preset-registry.ts +17 -2
  40. package/src/config/model-profile-activation.ts +3 -1
  41. package/src/config/model-profiles.ts +11 -3
  42. package/src/config/model-registry.ts +45 -17
  43. package/src/config/settings-schema.ts +8 -2
  44. package/src/exa/mcp-client.ts +41 -8
  45. package/src/exec/bash-shell-guardian.ts +214 -48
  46. package/src/gjc-runtime/session-state-sidecar.ts +88 -58
  47. package/src/hashline/execute.ts +51 -3
  48. package/src/hashline/parser.ts +63 -16
  49. package/src/internal-urls/docs-index.generated.ts +25 -25
  50. package/src/modes/acp/acp-agent.ts +136 -20
  51. package/src/modes/controllers/ssh-command-controller.ts +3 -1
  52. package/src/modes/interactive-mode.ts +10 -0
  53. package/src/modes/utils/context-usage.ts +2 -6
  54. package/src/prompts/tools/task.md +8 -27
  55. package/src/sdk/acp/adapter.ts +38 -3
  56. package/src/sdk/broker/doctor-restart.ts +38 -3
  57. package/src/sdk/broker/lifecycle.ts +408 -218
  58. package/src/sdk/broker/recovery-backoff.ts +135 -0
  59. package/src/sdk/broker/runtime.ts +83 -0
  60. package/src/sdk/broker/startup-budget.ts +6 -1
  61. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  62. package/src/sdk/bus/index.ts +308 -22
  63. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  64. package/src/sdk/client/client.ts +53 -35
  65. package/src/sdk/host/session-runtime.ts +99 -3
  66. package/src/sdk/prompt-watchdog.ts +17 -4
  67. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  68. package/src/session/agent-session.ts +79 -17
  69. package/src/session/internal/managed-session-storage.ts +23 -0
  70. package/src/task/index.ts +21 -66
  71. package/src/tools/bash.ts +39 -0
  72. package/src/tools/descriptors.ts +27 -1
  73. package/src/tools/eval-backends.ts +95 -0
  74. package/src/tools/eval.ts +3 -15
  75. package/src/tools/index.ts +9 -88
  76. package/src/tools/search.ts +2 -7
  77. package/src/tools/session-descriptions.ts +135 -0
  78. package/src/tools/tool-catalog.generated.ts +1 -1
  79. package/dist/types/cli/grep-cli.d.ts +0 -17
  80. package/dist/types/cli/shell-cli.d.ts +0 -8
  81. package/dist/types/commands/grep.d.ts +0 -42
  82. package/dist/types/commands/shell.d.ts +0 -21
  83. package/src/cli/grep-cli.ts +0 -160
  84. package/src/cli/shell-cli.ts +0 -176
  85. package/src/commands/grep.ts +0 -48
  86. package/src/commands/shell.ts +0 -29
@@ -165,6 +165,27 @@ function isRetryableReplacementReceiptCleanupError(error: unknown): boolean {
165
165
  return error instanceof Error && error.message === "managed_replace_receipt_cleanup_pending:io_error";
166
166
  }
167
167
 
168
+ /**
169
+ * A receipt retirement slot is named only from the receipt and predecessor
170
+ * identities, so every process sharing a managed scope — the publisher retiring
171
+ * its own receipt and each peer reconciler that listed it — claims the same
172
+ * slot. Native exact-unlink reports `quarantine_collision` from its no-replace
173
+ * claim before any rename, so a collision that carries no detached or retained
174
+ * path proves only that a peer owns the retirement right now: this process
175
+ * changed nothing, the receipt stays where a later reconciliation finds it, and
176
+ * the unrelated mutation that triggered the scan must not fail.
177
+ */
178
+ function isPeerOwnedReceiptRetirement(result: NativeExactUnlinkResult): boolean {
179
+ return (
180
+ !result.ok &&
181
+ result.code === "quarantine_collision" &&
182
+ result.detachedPath === undefined &&
183
+ result.retainedSuccessorPath === undefined &&
184
+ result.retainedPlaceholderPath === undefined &&
185
+ result.retainedUnknownPath === undefined
186
+ );
187
+ }
188
+
168
189
  /** A same-filesystem rename updates the moved root's ctime but no other tree identity. */
169
190
  function sameDirectoryTreeSnapshotAfterRename(
170
191
  left: NativeDirectoryTreeSnapshot,
@@ -1622,6 +1643,7 @@ export class ManagedSessionDescendantStore {
1622
1643
  sha256: placeholder.identity.sha256,
1623
1644
  quarantineName,
1624
1645
  });
1646
+ if (isPeerOwnedReceiptRetirement(removed)) return true;
1625
1647
  if (
1626
1648
  (!exactUnlinkCompleted(removed) && removed.code !== "not_found") ||
1627
1649
  removed.retainedSuccessorPath !== undefined ||
@@ -1660,6 +1682,7 @@ export class ManagedSessionDescendantStore {
1660
1682
  detachOnly: true,
1661
1683
  quarantineName,
1662
1684
  });
1685
+ if (isPeerOwnedReceiptRetirement(detached)) return;
1663
1686
  if (
1664
1687
  !detached.ok &&
1665
1688
  detached.code === "not_found" &&
package/src/task/index.ts CHANGED
@@ -32,11 +32,11 @@ import { resolveProfileBindings } from "../config/model-profiles";
32
32
  import { resolveAgentModelPatterns } from "../config/model-resolver";
33
33
  import type { Theme } from "../modes/theme/theme";
34
34
  import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
35
- import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
36
35
  import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" };
37
36
  import type { ForkContextSeed } from "../session/agent-session";
38
37
  import { splitSelectorThinkingSuffix } from "../thinking";
39
38
  import { formatBytes, formatDuration } from "../tools/render-utils";
39
+ import { renderTaskDescription } from "../tools/session-descriptions";
40
40
  import { escapeXmlAttribute } from "../utils/xml-escape";
41
41
  import {
42
42
  type AgentDefinition,
@@ -317,57 +317,26 @@ export {
317
317
  taskSchema,
318
318
  } from "./types";
319
319
 
320
- /**
321
- * Render the tool description from a cached agent list and current settings.
322
- */
323
320
  function hasAvailableIrcTool(session: ToolSession): boolean {
324
321
  return session.settings.get("irc.enabled") === true && session.getToolByName?.("irc") !== undefined;
325
322
  }
326
323
 
327
- function renderDescription(
328
- agents: AgentDefinition[],
329
- maxConcurrency: number,
330
- isolationEnabled: boolean,
331
- asyncEnabled: boolean,
332
- disabledAgents: string[],
333
- simpleMode: TaskSimpleMode,
334
- ircEnabled: boolean,
335
- parentSpawns: string,
336
- autoroutingActive: boolean,
337
- ): string {
338
- const spawningDisabled = parentSpawns === "";
339
- let filteredAgents = filterVisibleAgents(agents);
340
- filteredAgents =
341
- disabledAgents.length > 0 ? filteredAgents.filter(a => !disabledAgents.includes(a.name)) : filteredAgents;
342
- if (spawningDisabled) {
343
- filteredAgents = [];
344
- } else if (parentSpawns !== "*") {
345
- const allowed = new Set(
346
- parentSpawns
347
- .split(",")
348
- .map(s => s.trim())
349
- .filter(Boolean),
350
- );
351
- filteredAgents = filteredAgents.filter(a => allowed.has(a.name));
352
- }
353
- const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
354
- const description = prompt.render(taskDescriptionTemplate, {
355
- agents: filteredAgents,
356
- spawningDisabled,
357
- MAX_CONCURRENCY: maxConcurrency,
358
- isolationEnabled,
359
- asyncEnabled,
360
- contextEnabled,
361
- customSchemaEnabled,
362
- ircEnabled,
363
- defaultMode: simpleMode === "default",
364
- schemaFreeMode: simpleMode === "schema-free",
365
- independentMode: simpleMode === "independent",
366
- autoroutingActive,
367
- });
368
- return description;
324
+ function callableAgents(agents: readonly AgentDefinition[], session: ToolSession): AgentDefinition[] {
325
+ const disabledAgents = new Set(session.settings.get("task.disabledAgents"));
326
+ const parentSpawns = session.getSessionSpawns() ?? "*";
327
+ const allowedSpawns =
328
+ parentSpawns === "*"
329
+ ? undefined
330
+ : new Set(
331
+ parentSpawns
332
+ .split(",")
333
+ .map(agent => agent.trim())
334
+ .filter(Boolean),
335
+ );
336
+ return filterVisibleAgents(agents).filter(
337
+ agent => !disabledAgents.has(agent.name) && (allowedSpawns === undefined || allowedSpawns.has(agent.name)),
338
+ );
369
339
  }
370
-
371
340
  function createTaskModeError(text: string): AgentToolResult<TaskToolDetails> {
372
341
  return {
373
342
  content: [{ type: "text", text }],
@@ -614,22 +583,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
614
583
  return renderTaskCall(args as TaskParams, options, theme);
615
584
  }
616
585
 
617
- /** Dynamic description that reflects current disabled-agent settings */
618
586
  get description(): string {
619
- const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
620
- const maxConcurrency = this.session.settings.get("task.maxConcurrency");
621
- const isolationMode = this.session.settings.get("task.isolation.mode");
622
- return renderDescription(
623
- this.#discoveredAgents,
624
- maxConcurrency,
625
- isolationMode !== "none",
626
- true,
627
- disabledAgents,
628
- this.#getTaskSimpleMode(),
629
- hasAvailableIrcTool(this.session),
630
- this.session.getSessionSpawns() ?? "*",
631
- this.session.settings.getEffectiveAutorouting().active,
632
- );
587
+ return renderTaskDescription(this.session);
633
588
  }
634
589
  readonly #sessionRepositoryBinding: RepositoryBinding;
635
590
  #testRunSubprocess: typeof runSubprocess | undefined;
@@ -945,8 +900,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
945
900
  const agent = getAgent(this.#discoveredAgents, params.agent);
946
901
  if (!agent) {
947
902
  const available =
948
- filterVisibleAgents(this.#discoveredAgents)
949
- .map(a => a.name)
903
+ callableAgents(this.#discoveredAgents, this.session)
904
+ .map(agent => agent.name)
950
905
  .join(", ") || "none";
951
906
  return {
952
907
  content: [{ type: "text", text: `Unknown agent "${params.agent}". Available: ${available}` }],
@@ -1714,8 +1669,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1714
1669
  const agent = getAgent(agents, agentName);
1715
1670
  if (!agent) {
1716
1671
  const available =
1717
- filterVisibleAgents(agents)
1718
- .map(a => a.name)
1672
+ callableAgents(agents, this.session)
1673
+ .map(agent => agent.name)
1719
1674
  .join(", ") || "none";
1720
1675
  return {
1721
1676
  content: [
package/src/tools/bash.ts CHANGED
@@ -10,10 +10,26 @@ import { type BashArtifactSaveResult, type BashResult, executeBash } from "../ex
10
10
 
11
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
12
  import { buildGjcRuntimeSessionEnv } from "../gjc-runtime/goal-mode-request";
13
+ import {
14
+ MANAGED_OWNER_PREDECESSOR_GENERATION_ENV,
15
+ MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV,
16
+ MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
17
+ MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
18
+ MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
19
+ } from "../gjc-runtime/managed-owner-admission";
20
+ import {
21
+ MANAGED_OWNER_CHILD_TOKEN_ENV,
22
+ MANAGED_OWNER_GENERATION_ENV,
23
+ MANAGED_OWNER_INCARNATION_ENV,
24
+ MANAGED_OWNER_RUN_ID_ENV,
25
+ MANAGED_OWNER_STATE_DIR_ENV,
26
+ } from "../gjc-runtime/managed-owner-supervisor";
13
27
  import {
14
28
  GJC_RALPLAN_ARTIFACT_ENV,
15
29
  GJC_RESTRICTED_ROLE_AGENT_BASH_ENV,
16
30
  } from "../gjc-runtime/restricted-role-agent-bash";
31
+ import { GJC_TMUX_OWNER_SERVER_KEY_ENV } from "../gjc-runtime/session-state-sidecar";
32
+ import { GJC_TMUX_LAUNCHED_ENV } from "../gjc-runtime/windows-powershell-command";
17
33
  import { InternalUrlRouter } from "../internal-urls";
18
34
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
19
35
  import { highlightCode, type Theme } from "../modes/theme/theme";
@@ -94,6 +110,27 @@ const ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
94
110
  const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
95
111
  const MASTER_CAPABILITY_ENV = "GJC_MASTER_CAPABILITY";
96
112
  const MASTER_OWNER_SESSION_ENV = "GJC_MASTER_OWNER_SESSION_ID";
113
+ // Managed-owner env vars that must be scrubbed from child processes.
114
+ // Exported for testing the env scrubbing behavior.
115
+ export const MANAGED_OWNER_BASH_ENV = [
116
+ MANAGED_OWNER_STATE_DIR_ENV,
117
+ MANAGED_OWNER_GENERATION_ENV,
118
+ MANAGED_OWNER_RUN_ID_ENV,
119
+ MANAGED_OWNER_INCARNATION_ENV,
120
+ MANAGED_OWNER_CHILD_TOKEN_ENV,
121
+ MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
122
+ MANAGED_OWNER_PREDECESSOR_GENERATION_ENV,
123
+ MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
124
+ MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV,
125
+ MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
126
+ // The rest of the tmux-owner tuple (tmux-sessions.ts managedEnvironment): leaving
127
+ // either behind makes ownerTerminalContextFromEnvironment() report "invalid" in a
128
+ // nested gjc, because a server key or GJC_TMUX_LAUNCHED=1 without generation/state
129
+ // dir is an incomplete owner context.
130
+ GJC_TMUX_OWNER_SERVER_KEY_ENV,
131
+ GJC_TMUX_LAUNCHED_ENV,
132
+ ] as const;
133
+
97
134
  const COORDINATOR_ONLY_BASH_ENV = [
98
135
  "GJC_COORDINATOR_SESSION_STATE_FILE",
99
136
  "GJC_COORDINATOR_SESSION_ID",
@@ -102,6 +139,8 @@ const COORDINATOR_ONLY_BASH_ENV = [
102
139
  "GJC_COORDINATOR_SESSION_READINESS_FILE",
103
140
  "GJC_COORDINATOR_SIDECAR_SIGNATURE_REQUIRED",
104
141
  "GJC_COORDINATOR_SIDECAR_KEY_ID",
142
+ // Managed-owner env family from managed-owner-supervisor.ts and managed-owner-admission.ts
143
+ ...MANAGED_OWNER_BASH_ENV,
105
144
  ] as const;
106
145
  const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
107
146
  const ACP_RELEASE_TIMEOUT_MS = 1_000;
@@ -11,6 +11,12 @@ import {
11
11
  validateDeferredAskArguments,
12
12
  validateDeferredTodoArguments,
13
13
  } from "./descriptor-validation";
14
+ import {
15
+ evalToolDescriptionForSession,
16
+ renderTaskDescription,
17
+ searchToolDescriptionForSession,
18
+ taskParametersForSession,
19
+ } from "./session-descriptions";
14
20
  import { TOOL_CATALOG, type ToolCatalogEntry } from "./tool-catalog.generated";
15
21
  import { ToolError } from "./tool-errors";
16
22
 
@@ -33,6 +39,11 @@ export interface ToolDescriptorMetadata {
33
39
  readonly intent?: AgentTool<any, any, any>["intent"];
34
40
  readonly parametersForSession?: (session?: ToolSession) => TSchema;
35
41
  readonly parameters?: TSchema;
42
+ /**
43
+ * Renders the description the loaded tool will report for this session. Tools whose prompt text
44
+ * depends on session settings need it, or the advertised text changes on first load (#5992).
45
+ */
46
+ readonly descriptionForSession?: (session: ToolSession) => string;
36
47
  readonly description?: string;
37
48
  readonly label?: string;
38
49
  readonly customWireName?: string;
@@ -121,8 +132,10 @@ export class LazyAgentTool implements AgentTool<any, any, any> {
121
132
  return this.#tool?.name ?? this.descriptor.metadata.name;
122
133
  }
123
134
  get description(): string {
135
+ const session = this.#session;
124
136
  return (
125
137
  this.#tool?.description ??
138
+ (session ? this.descriptor.metadata.descriptionForSession?.(session) : undefined) ??
126
139
  this.descriptor.metadata.description ??
127
140
  this.descriptor.presentation.summary ??
128
141
  this.descriptor.metadata.summary ??
@@ -393,6 +406,7 @@ function descriptor(spec: DescriptorSpec): ToolDescriptor {
393
406
  inline: catalog?.inline ?? spec.inline,
394
407
  rawArgumentValidation: spec.rawArgumentValidation,
395
408
  parametersForSession: spec.parametersForSession,
409
+ descriptionForSession: spec.descriptionForSession,
396
410
  intent: catalog?.intent ?? spec.intent,
397
411
  description: catalog?.description ?? spec.description,
398
412
  parameters: spec.parameters ?? (catalog?.parameters as TSchema | undefined),
@@ -465,6 +479,15 @@ const names: Array<[name: string, label: string, summary: string | undefined, lo
465
479
  ["move_session", "Move Session", undefined, "essential"],
466
480
  ];
467
481
 
482
+ const sessionDescriptions: Readonly<Record<string, ToolDescriptorMetadata["descriptionForSession"]>> = {
483
+ eval: evalToolDescriptionForSession,
484
+ search: searchToolDescriptionForSession,
485
+ task: renderTaskDescription,
486
+ };
487
+
488
+ const sessionParameters: Readonly<Record<string, ToolDescriptorMetadata["parametersForSession"]>> = {
489
+ task: taskParametersForSession,
490
+ };
468
491
  const descriptorRawArgumentValidations: Readonly<Record<string, ToolDescriptorMetadata["rawArgumentValidation"]>> = {
469
492
  ask: validateDeferredAskArguments,
470
493
  todo_write: validateDeferredTodoArguments,
@@ -487,7 +510,10 @@ const builtins = names
487
510
  : undefined,
488
511
  parameters: name === "ask" ? deferredAskParameters : undefined,
489
512
  parametersForSession:
490
- name === "ask" ? session => selectAskParameters(session?.getDeepInterviewAskStage?.()) : undefined,
513
+ name === "ask"
514
+ ? session => selectAskParameters(session?.getDeepInterviewAskStage?.())
515
+ : sessionParameters[name],
516
+ descriptionForSession: sessionDescriptions[name],
491
517
  rawArgumentValidation: descriptorRawArgumentValidations[name],
492
518
  intent: deferredIntentPolicies[name],
493
519
  loader: loaders[name],
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Eval backend allowance (`GJC_PY`, legacy `PI_PY` / `PI_JS`, then `eval.py` / `eval.js` settings).
3
+ * Kept out of the tools index so lightweight modules, such as the pre-load tool descriptions, can
4
+ * resolve it without importing the whole registry.
5
+ */
6
+ import { $env } from "@gajae-code/utils";
7
+ import type { ToolSession } from ".";
8
+
9
+ export interface EvalBackendsAllowance {
10
+ python: boolean;
11
+ js: boolean;
12
+ }
13
+
14
+ /**
15
+ * Parse the `GJC_PY` multi-value token into per-backend booleans.
16
+ *
17
+ * Tokens (case-insensitive):
18
+ * - `0` / `bash` → JavaScript only (`{ py: false, js: true }`)
19
+ * - `1` / `py` → Python only (`{ py: true, js: false }`)
20
+ * - `js` → JavaScript only (`{ py: false, js: true }`)
21
+ * - `mix` / `both` → both backends (`{ py: true, js: true }`)
22
+ *
23
+ * Returns `null` when `GJC_PY` is unset, empty, or holds an unrecognized
24
+ * token, so the caller can fall back to the legacy `PI_PY` / `PI_JS` flags or
25
+ * per-key settings. This matches the documented contract that invalid values
26
+ * are ignored.
27
+ */
28
+ export function parseGjcPy(env: Record<string, string | undefined>): { py: boolean; js: boolean } | null {
29
+ const raw = env.GJC_PY;
30
+ if (raw === undefined) return null;
31
+ const token = raw.trim().toLowerCase();
32
+ if (token === "") return null;
33
+ switch (token) {
34
+ case "0":
35
+ case "bash":
36
+ return { py: false, js: true };
37
+ case "1":
38
+ case "py":
39
+ return { py: true, js: false };
40
+ case "js":
41
+ return { py: false, js: true };
42
+ case "mix":
43
+ case "both":
44
+ return { py: true, js: true };
45
+ default:
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function isTruthyPythonFlag(value: string | undefined): boolean {
51
+ return value !== undefined && ["1", "true", "yes", "on", "y"].includes(value.trim().toLowerCase());
52
+ }
53
+
54
+ /**
55
+ * Parse legacy `PI_PY` / `PI_JS` boolean flags. Each is a boolean flag; unset
56
+ * means "not specified, defer to settings". Returns `null` when neither is set
57
+ * so the caller can fall through to `readEvalBackendsAllowance` per key.
58
+ */
59
+ function parseLegacyEvalEnvFlags(env: Record<string, string | undefined>): EvalBackendsAllowance | null {
60
+ const pyEnv = env.PI_PY;
61
+ const jsEnv = env.PI_JS;
62
+ if (pyEnv === undefined && jsEnv === undefined) return null;
63
+ return {
64
+ python: pyEnv === undefined ? true : isTruthyPythonFlag(pyEnv),
65
+ js: jsEnv === undefined ? true : isTruthyPythonFlag(jsEnv),
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Resolve eval-backend allowance from environment only. `GJC_PY` wins when set
71
+ * to a recognized token; otherwise the legacy `PI_PY` / `PI_JS` flags apply.
72
+ * Returns `null` when no env override is set so the caller can defer to settings.
73
+ */
74
+ export function resolveEvalBackendsFromEnv(env: Record<string, string | undefined>): EvalBackendsAllowance | null {
75
+ const gjc = parseGjcPy(env);
76
+ if (gjc) return { python: gjc.py, js: gjc.js };
77
+ return parseLegacyEvalEnvFlags(env);
78
+ }
79
+
80
+ /** Read per-backend allowance from settings (defaults true). */
81
+ export function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance {
82
+ return {
83
+ python: session.settings.get("eval.py") ?? true,
84
+ js: session.settings.get("eval.js") ?? true,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Materialize the active eval backend allowance. `GJC_PY` takes precedence
90
+ * over the legacy `PI_PY` / `PI_JS` env flags, which in turn override the
91
+ * per-key settings (defaults true). When no env override is set, settings win.
92
+ */
93
+ export function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance {
94
+ return resolveEvalBackendsFromEnv($env) ?? readEvalBackendsAllowance(session);
95
+ }
package/src/tools/eval.ts CHANGED
@@ -2,7 +2,6 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { ImageContent } from "@gajae-code/ai/core";
3
3
  import type { Component } from "@gajae-code/tui";
4
4
  import { Markdown, Text } from "@gajae-code/tui";
5
- import { prompt } from "@gajae-code/utils";
6
5
  import * as z from "zod/v4";
7
6
  import { jsBackend, pythonBackend } from "../eval";
8
7
  import type { ExecutorBackend } from "../eval/backend";
@@ -10,7 +9,6 @@ import type { EvalCellResult, EvalDisplayOutput, EvalLanguage, EvalStatusEvent,
10
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
10
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
12
11
  import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
13
- import evalDescription from "../prompts/tools/eval.md" with { type: "text" };
14
12
  import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary, TailBuffer } from "../session/streaming-output";
15
13
  import { getTreeBranch, getTreeContinuePrefix, renderCodeCell } from "../tui";
16
14
  import { resolveEvalBackends, type ToolSession } from ".";
@@ -21,6 +19,7 @@ import {
21
19
  stripOutputNotice,
22
20
  } from "./output-meta";
23
21
  import { formatTitle, replaceTabs, shortenPath, truncateToWidth, wrapBrackets } from "./render-utils";
22
+ import { evalToolDescriptionForSession } from "./session-descriptions";
24
23
  import { ToolAbortError, ToolError } from "./tool-errors";
25
24
  import { toolResult } from "./tool-result";
26
25
  import { clampTimeout } from "./tool-timeouts";
@@ -134,16 +133,7 @@ function renderJsonTree(value: unknown, theme: Theme, expanded: boolean, maxDept
134
133
  return renderNode(value, "", 0, true);
135
134
  }
136
135
 
137
- export interface EvalToolDescriptionOptions {
138
- py?: boolean;
139
- js?: boolean;
140
- }
141
-
142
- export function getEvalToolDescription(options: EvalToolDescriptionOptions = {}): string {
143
- const py = options.py ?? true;
144
- const js = options.js ?? true;
145
- return prompt.render(evalDescription, { py, js });
146
- }
136
+ export { type EvalToolDescriptionOptions, getEvalToolDescription } from "./session-descriptions";
147
137
 
148
138
  export interface EvalToolOptions {
149
139
  proxyExecutor?: EvalProxyExecutor;
@@ -204,9 +194,7 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
204
194
  readonly loadMode = "discoverable";
205
195
  readonly label = "Eval";
206
196
  get description(): string {
207
- if (!this.session) return getEvalToolDescription();
208
- const backends = resolveEvalBackends(this.session);
209
- return getEvalToolDescription({ py: backends.python, js: backends.js });
197
+ return evalToolDescriptionForSession(this.session);
210
198
  }
211
199
  readonly parameters = evalSchema;
212
200
  readonly concurrency = "exclusive";
@@ -1,6 +1,6 @@
1
1
  import type { AgentTelemetryConfig, AgentTool } from "@gajae-code/agent-core";
2
2
  import type { Model, ServiceTier, ToolChoice } from "@gajae-code/ai/core";
3
- import { $env, logger } from "@gajae-code/utils";
3
+ import { logger } from "@gajae-code/utils";
4
4
  import type { AsyncJobManager, FoldReason } from "../async";
5
5
  import type { PromptTemplate } from "../config/prompt-templates";
6
6
  import type { Settings } from "../config/settings";
@@ -37,6 +37,7 @@ import {
37
37
  resolveEffectiveDiscoveryMode,
38
38
  type ToolAvailabilityContext,
39
39
  } from "./descriptors";
40
+ import { resolveEvalBackends } from "./eval-backends";
40
41
  import { wrapToolWithMetaNotice } from "./output-meta";
41
42
  import type { TodoPhase } from "./todo-write";
42
43
 
@@ -491,93 +492,13 @@ const GOAL_MODE_TOOL_NAMES = [] as const;
491
492
 
492
493
  export type ToolName = keyof typeof BUILTIN_TOOLS;
493
494
 
494
- export interface EvalBackendsAllowance {
495
- python: boolean;
496
- js: boolean;
497
- }
498
-
499
- /**
500
- * Parse the `GJC_PY` multi-value token into per-backend booleans.
501
- *
502
- * Tokens (case-insensitive):
503
- * - `0` / `bash` → JavaScript only (`{ py: false, js: true }`)
504
- * - `1` / `py` → Python only (`{ py: true, js: false }`)
505
- * - `js` → JavaScript only (`{ py: false, js: true }`)
506
- * - `mix` / `both` → both backends (`{ py: true, js: true }`)
507
- *
508
- * Returns `null` when `GJC_PY` is unset, empty, or holds an unrecognized
509
- * token, so the caller can fall back to the legacy `PI_PY` / `PI_JS` flags or
510
- * per-key settings. This matches the documented contract that invalid values
511
- * are ignored.
512
- */
513
- export function parseGjcPy(env: Record<string, string | undefined>): { py: boolean; js: boolean } | null {
514
- const raw = env.GJC_PY;
515
- if (raw === undefined) return null;
516
- const token = raw.trim().toLowerCase();
517
- if (token === "") return null;
518
- switch (token) {
519
- case "0":
520
- case "bash":
521
- return { py: false, js: true };
522
- case "1":
523
- case "py":
524
- return { py: true, js: false };
525
- case "js":
526
- return { py: false, js: true };
527
- case "mix":
528
- case "both":
529
- return { py: true, js: true };
530
- default:
531
- return null;
532
- }
533
- }
534
-
535
- function isTruthyPythonFlag(value: string | undefined): boolean {
536
- return value !== undefined && ["1", "true", "yes", "on", "y"].includes(value.trim().toLowerCase());
537
- }
538
-
539
- /**
540
- * Parse legacy `PI_PY` / `PI_JS` boolean flags. Each is a boolean flag; unset
541
- * means "not specified, defer to settings". Returns `null` when neither is set
542
- * so the caller can fall through to `readEvalBackendsAllowance` per key.
543
- */
544
- function parseLegacyEvalEnvFlags(env: Record<string, string | undefined>): EvalBackendsAllowance | null {
545
- const pyEnv = env.PI_PY;
546
- const jsEnv = env.PI_JS;
547
- if (pyEnv === undefined && jsEnv === undefined) return null;
548
- return {
549
- python: pyEnv === undefined ? true : isTruthyPythonFlag(pyEnv),
550
- js: jsEnv === undefined ? true : isTruthyPythonFlag(jsEnv),
551
- };
552
- }
553
-
554
- /**
555
- * Resolve eval-backend allowance from environment only. `GJC_PY` wins when set
556
- * to a recognized token; otherwise the legacy `PI_PY` / `PI_JS` flags apply.
557
- * Returns `null` when no env override is set so the caller can defer to settings.
558
- */
559
- export function resolveEvalBackendsFromEnv(env: Record<string, string | undefined>): EvalBackendsAllowance | null {
560
- const gjc = parseGjcPy(env);
561
- if (gjc) return { python: gjc.py, js: gjc.js };
562
- return parseLegacyEvalEnvFlags(env);
563
- }
564
-
565
- /** Read per-backend allowance from settings (defaults true). */
566
- export function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance {
567
- return {
568
- python: session.settings.get("eval.py") ?? true,
569
- js: session.settings.get("eval.js") ?? true,
570
- };
571
- }
572
-
573
- /**
574
- * Materialize the active eval backend allowance. `GJC_PY` takes precedence
575
- * over the legacy `PI_PY` / `PI_JS` env flags, which in turn override the
576
- * per-key settings (defaults true). When no env override is set, settings win.
577
- */
578
- export function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance {
579
- return resolveEvalBackendsFromEnv($env) ?? readEvalBackendsAllowance(session);
580
- }
495
+ export {
496
+ type EvalBackendsAllowance,
497
+ parseGjcPy,
498
+ readEvalBackendsAllowance,
499
+ resolveEvalBackends,
500
+ resolveEvalBackendsFromEnv,
501
+ } from "./eval-backends";
581
502
 
582
503
  /**
583
504
  * Create tools from the descriptor registry.
@@ -5,12 +5,10 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
5
5
  import type { GrepMatch, GrepResult, grep as grepFn } from "@gajae-code/natives";
6
6
  import type { Component } from "@gajae-code/tui";
7
7
  import { Text } from "@gajae-code/tui";
8
- import { prompt } from "@gajae-code/utils";
9
8
  import * as z from "zod/v4";
10
9
  import { getFileReadCache } from "../edit/file-read-cache";
11
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
11
  import type { Theme } from "../modes/theme/theme";
13
- import searchDescription from "../prompts/tools/search.md" with { type: "text" };
14
12
  import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "../session/streaming-output";
15
13
  import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
16
14
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
@@ -35,6 +33,7 @@ import {
35
33
  PREVIEW_LIMITS,
36
34
  splitGroupsByBlankLine,
37
35
  } from "./render-utils";
36
+ import { searchToolDescriptionForSession } from "./session-descriptions";
38
37
  import { ToolAbortError, ToolError } from "./tool-errors";
39
38
  import { toolResult } from "./tool-result";
40
39
 
@@ -259,11 +258,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
259
258
  readonly loadMode = "discoverable";
260
259
  readonly summary = "Search file contents using ripgrep (fast text search)";
261
260
  get description(): string {
262
- const displayMode = resolveFileDisplayMode(this.session);
263
- return prompt.render(searchDescription, {
264
- IS_HL_MODE: displayMode.hashLines,
265
- IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
266
- });
261
+ return searchToolDescriptionForSession(this.session);
267
262
  }
268
263
  readonly parameters = searchSchema;
269
264
  readonly strict = true;