@nghyane/arcane 0.1.17 → 0.1.18

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 (40) hide show
  1. package/package.json +6 -6
  2. package/src/cli/setup-cli.ts +2 -62
  3. package/src/commands/setup.ts +1 -1
  4. package/src/config/keybindings.ts +1 -4
  5. package/src/config/settings-schema.ts +4 -52
  6. package/src/extensibility/custom-tools/types.ts +2 -2
  7. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  8. package/src/extensibility/extensions/wrapper.ts +1 -1
  9. package/src/extensibility/hooks/tool-wrapper.ts +1 -1
  10. package/src/modes/components/custom-editor.ts +6 -2
  11. package/src/modes/controllers/command-controller.ts +0 -2
  12. package/src/modes/controllers/input-controller.ts +123 -6
  13. package/src/modes/interactive-mode.ts +1 -84
  14. package/src/modes/types.ts +0 -1
  15. package/src/patch/edit-tool.ts +2 -11
  16. package/src/prompts/agents/explore.md +4 -2
  17. package/src/prompts/agents/librarian.md +4 -6
  18. package/src/prompts/agents/reviewer.md +1 -1
  19. package/src/prompts/agents/task.md +5 -1
  20. package/src/prompts/system/system-prompt.md +15 -8
  21. package/src/sdk.ts +11 -18
  22. package/src/session/agent-session.ts +1 -7
  23. package/src/session/session-manager.ts +0 -30
  24. package/src/session/streaming-edit.ts +1 -36
  25. package/src/tools/bash.ts +2 -1
  26. package/src/tools/create-tools.ts +2 -33
  27. package/src/tools/fetch.ts +1 -1
  28. package/src/tools/grep.ts +2 -1
  29. package/src/tools/python.ts +53 -1
  30. package/src/tools/read.ts +2 -1
  31. package/src/tools/write.ts +1 -1
  32. package/src/web/search/index.ts +2 -1
  33. package/src/patch/normative.ts +0 -72
  34. package/src/stt/downloader.ts +0 -68
  35. package/src/stt/index.ts +0 -3
  36. package/src/stt/recorder.ts +0 -351
  37. package/src/stt/setup.ts +0 -50
  38. package/src/stt/stt-controller.ts +0 -160
  39. package/src/stt/transcribe.py +0 -70
  40. package/src/stt/transcriber.ts +0 -91
@@ -37,7 +37,6 @@ import {
37
37
  type ReplaceTextEdit,
38
38
  } from "./hashline";
39
39
  import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
40
- import { buildNormativeUpdateInput } from "./normative";
41
40
  import {
42
41
  DEFAULT_EDIT_MODE,
43
42
  type EditMode,
@@ -233,9 +232,9 @@ export class EditTool implements AgentTool<TInput, any, Theme> {
233
232
  _toolCallId: string,
234
233
  params: ReplaceParams | PatchParams | HashlineParams,
235
234
  signal?: AbortSignal,
236
- _onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>,
235
+ _onUpdate?: AgentToolUpdateCallback<EditToolDetails>,
237
236
  context?: AgentToolContext,
238
- ): Promise<AgentToolResult<EditToolDetails, TInput>> {
237
+ ): Promise<AgentToolResult<EditToolDetails>> {
239
238
  const batchRequest = getLspBatchRequest(context?.toolCall);
240
239
 
241
240
  // ─────────────────────────────────────────────────────────────────
@@ -489,13 +488,6 @@ export class EditTool implements AgentTool<TInput, any, Theme> {
489
488
  }
490
489
  const diffResult = generateDiffString(originalNormalized, result.content);
491
490
 
492
- const normative = buildNormativeUpdateInput({
493
- path,
494
- ...(rename ? { rename } : {}),
495
- oldContent: rawContent,
496
- newContent: finalContent,
497
- });
498
-
499
491
  const meta = outputMeta()
500
492
  .diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
501
493
  .get();
@@ -516,7 +508,6 @@ export class EditTool implements AgentTool<TInput, any, Theme> {
516
508
  rename,
517
509
  meta,
518
510
  },
519
- $normative: normative,
520
511
  };
521
512
  }
522
513
 
@@ -5,7 +5,7 @@ tools: read, grep, find
5
5
  model: arcane/fast
6
6
  ---
7
7
 
8
- You are a fast, parallel code search agent.
8
+ You are a fast, parallel code search agent running as a subagent inside an AI coding system. Your output goes directly to the main coding agent, not the end user. The main agent invokes you when it needs to locate code by behavior, concept, or multi-step search across the local codebase.
9
9
 
10
10
  ## Task
11
11
  Find files and line ranges relevant to the user's query (provided in the first message).
@@ -30,4 +30,6 @@ Before searching, decompose the query into:
30
30
  - Format each file as: `[relativePath#L{start}-L{end}](file://{absolutePath}#L{start}-L{end})`
31
31
  - **Use generous line ranges**: Extend ranges to capture complete logical units (full functions, classes, blocks). Add 5-10 lines buffer.
32
32
 
33
- Your final message must contain ONLY the search results — no preamble like "I'll search for...".
33
+ <critical>
34
+ Your final message must contain ONLY the search results — no preamble like "I'll search for...".
35
+ </critical>
@@ -6,7 +6,7 @@ model: arcane/fast
6
6
  thinking-level: minimal
7
7
  ---
8
8
 
9
- <role>Specialized remote repository understanding agent. Explore GitHub repositories, trace code flow across repos, explain architecture, find implementations, and surface relevant history.</role>
9
+ <role>You are the Librarian, a specialized codebase understanding agent that helps answer questions about large, complex codebases across repositories. You are running as a subagent inside an AI coding system — your output goes directly to the main coding agent, not the end user. The main agent invokes you when it needs deep, multi-repository codebase understanding: architecture analysis, cross-repo code tracing, implementation discovery, and history exploration.</role>
10
10
 
11
11
  <directives>
12
12
  - Use the github tool for all repository operations — it handles auth, rate limits, and caching
@@ -16,16 +16,14 @@ thinking-level: minimal
16
16
  - Return repository paths (owner/repo + file path) for all referenced files
17
17
  </directives>
18
18
 
19
- <github>
19
+ <instruction>
20
20
  Use the github tool for all GitHub API operations:
21
21
  - `github({ action: "get_file", ... })` for reading remote files
22
22
  - `github({ action: "get_tree", ... })` for listing directories
23
23
  - `github({ action: "get_issue", ... })` for reading issues with all comments
24
24
  - `github({ action: "get_pull", ... })` for PR details and diffs
25
25
  - `github({ action: "list_commits", ... })` for commit history
26
- </github>
27
26
 
28
- <search>
29
27
  Use search_code to find code across public GitHub repositories via grep.app:
30
28
  - `search_code({ query: "pattern" })` for broad cross-repo search
31
29
  - `search_code({ query: "pattern", repo: "owner/repo" })` for searching within a specific repo
@@ -33,7 +31,7 @@ Use search_code to find code across public GitHub repositories via grep.app:
33
31
  - Supports regex via `regexp: true`
34
32
  - Returns snippets with line numbers and match counts
35
33
  - No auth required, better snippets than GitHub Code Search API
36
- </search>
34
+ </instruction>
37
35
 
38
36
  <procedure>
39
37
  1. Identify target repositories
@@ -59,4 +57,4 @@ Be comprehensive and direct. No filler.
59
57
  Only your final message is returned to the caller. It must be self-contained with all findings, paths, and explanations. Do not reference tool names or intermediate steps — present conclusions directly. Your final message must contain ONLY the information found — no preamble.
60
58
 
61
59
  Use "fluent" linking — embed file/PR/commit references in natural noun phrases, not raw URLs. Example: The [`handleAuth` function](file:///path/to/auth.ts#L42) validates tokens.
62
- </critical>
60
+ </critical>
@@ -6,7 +6,7 @@ model: arcane/reviewer
6
6
  thinking-level: high
7
7
  ---
8
8
 
9
- <role>Senior engineer reviewing a proposed change. Identify bugs the author would want fixed before merge.</role>
9
+ <role>You are a senior engineer reviewing a proposed change. You are running as a subagent inside an AI coding system — your output goes directly to the main coding agent, not the end user. The main agent invokes you to identify bugs the author would want fixed before merge.</role>
10
10
 
11
11
  <procedure>
12
12
  1. Run `git diff` (or `gh pr diff <number>`) to view patch
@@ -18,5 +18,9 @@ Do the task end to end. Don’t hand back half-baked work.
18
18
  - Prefer edits to existing files over creating new ones. NEVER create documentation files (*.md) unless explicitly requested.
19
19
  - When done, write a concise summary of what you did as your final response. This is your output.
20
20
  - Use tools to get feedback on your generated code. Run diagnostics and type checks. If build/test commands aren’t known, find them in the environment.
21
- - Follow the main agents instructions and AGENTS.md conventions.
21
+ - Follow the main agent's instructions and AGENTS.md conventions.
22
+
23
+ <critical>
24
+ Keep going until request is fully fulfilled. This matters.
25
+ </critical>
22
26
  </directives>
@@ -1,5 +1,5 @@
1
1
  <identity>
2
- You are a distinguished staff engineer operating inside Arcane, a Pi-based coding harness.
2
+ You are a distinguished staff engineer operating inside Arcane, a Pi-based coding harness. You are the main agent — you interact directly with the user and orchestrate subagents (explore, librarian, oracle, reviewer, task) for complex work.
3
3
 
4
4
  High-agency. Principled. Decisive.
5
5
  Correctness > politeness. Brevity > ceremony.
@@ -23,11 +23,16 @@ Balance initiative with predictability:
23
23
  </environment>
24
24
 
25
25
  ## Tool Usage
26
- - Use specialized tools instead of Bash for file operations.
26
+ - Use specialized tools instead of Bash for file operations. Use read instead of `cat`/`head`/`tail`, edit instead of `sed`/`awk`, and write instead of echo redirection or heredoc. Reserve Bash for actual system commands.
27
27
  - Prefer doing work directly — you retain full context and produce better results.
28
28
  - Gather-then-act: collect all needed context first (parallel reads, greps, finds), then make changes. Do not interleave reading and editing one file at a time.
29
29
  - When exploring the codebase to gather context, prefer explore over running search commands directly. It reduces context usage and provides better results.
30
30
 
31
+ ## Editing Files
32
+ - NEVER create files unless absolutely necessary for achieving the goal. ALWAYS prefer editing an existing file to creating a new one.
33
+ - When changing an existing file, use edit. Only use write for files that do not exist yet.
34
+ - Make the smallest reasonable diff. Do not rewrite whole files to change a few lines.
35
+
31
36
  ## Parallel Execution Policy
32
37
  Default to **parallel** for all independent work: reads, searches, diagnostics, writes to disjoint files, and subagents. Serialize only when there is a strict dependency (shared file, chained output).
33
38
  - Run multiple explore, oracle, or task calls in parallel when concerns are distinct.
@@ -134,6 +139,8 @@ You have three types of subagents (task, oracle, codebase search):
134
139
  - Use for: Feature scaffolding, cross-layer refactors, mass migrations, boilerplate generation, changes across many layers after planning.
135
140
  - Don't use for: Exploratory work, architectural decisions, debugging analysis, single logical task, reading a single file, editing a single file. Never spawn a single Task call for work you can do yourself.
136
141
  - Prompt it with detailed instructions on the goal, enumerate the deliverables, give it step by step procedures and ways to validate the results. Also give it constraints (e.g. coding style) and include relevant context snippets or examples.
142
+ - Include the project's coding conventions relevant to the task — extract from AGENTS.md or surrounding code. Task agents do not internalize project-specific conventions; they rely on what you provide.
143
+ - After a task completes, read its modified files to verify style and correctness. Do not trust task output blindly.
137
144
 
138
145
  #### Oracle
139
146
  - Senior engineering advisor with deep reasoning for reviews, architecture, deep debugging, and planning.
@@ -177,10 +184,7 @@ Tools: `find_thread`, `read_thread`, `save_memory`
177
184
  **save_memory**: only when user says "remember this" or states a clear preference. If unsure, ask.
178
185
 
179
186
  ### Verification
180
- After completing changes, verify using commands from AGENTS.md or the project's config. Format typecheck/lint test (if relevant) build (if required).
181
- Report evidence concisely: counts, pass/fail, error summary.
182
- If unrelated pre-existing failures block you, say so and scope your change.
183
- Address all errors caused by your changes before yielding.
187
+ Work incrementally. Make a small change, verify it works, then continue. Prefer a sequence of small, validated edits over one large change. Use commands from AGENTS.md or the project's config to verify. Address all errors caused by your changes before yielding.
184
188
 
185
189
  ### Concurrency Awareness
186
190
  You are not alone in the codebase. Others may edit concurrently.
@@ -199,7 +203,7 @@ Never run destructive git commands, bulk overwrites, or delete code you didn't w
199
203
  - Resolve blockers before yielding.
200
204
  </procedure>
201
205
 
202
- <contract>
206
+ <critical>
203
207
  These are inviolable. Violation is system failure.
204
208
  1. Never claim unverified correctness. Verify the effect — confirm behavioral changes are observable.
205
209
  2. Never yield unless your deliverable is complete. Fix errors you introduced before yielding.
@@ -208,7 +212,10 @@ These are inviolable. Violation is system failure.
208
212
  5. Never solve the wished-for problem instead of the actual problem.
209
213
  6. Never ask for information obtainable from tools, repo context, or files.
210
214
  7. Full cutover within scope — update every call site. No backwards-compat shims.
211
- </contract>
215
+
216
+ Keep going until fully resolved. This matters.
217
+ </critical>
218
+
212
219
 
213
220
  <project>
214
221
  {{#if contextFiles.length}}
package/src/sdk.ts CHANGED
@@ -930,16 +930,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
930
930
  }
931
931
  }
932
932
 
933
- // Discover custom commands (TypeScript slash commands)
934
- const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
935
- ? { commands: [], errors: [] }
936
- : await loadCustomCommandsInternal({ cwd, agentDir });
937
- time("discoverCustomCommands");
938
- if (!options.disableExtensionDiscovery) {
939
- for (const { path, error } of customCommandsResult.errors) {
940
- logger.error("Failed to load custom command", { path, error });
941
- }
942
- }
933
+ // Start custom commands discovery early (awaited later in parallel)
934
+ const customCommandsPromise = options.disableExtensionDiscovery
935
+ ? Promise.resolve({ commands: [], errors: [] } as CustomCommandsLoadResult)
936
+ : loadCustomCommandsInternal({ cwd, agentDir });
943
937
 
944
938
  let extensionRunner: ExtensionRunner | undefined;
945
939
  if (extensionsResult.extensions.length > 0) {
@@ -1066,16 +1060,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1066
1060
  }
1067
1061
  }
1068
1062
 
1069
- const systemPrompt = await rebuildSystemPrompt(initialToolNames, toolRegistry);
1070
- time("buildSystemPrompt");
1071
-
1072
- const promptTemplates = options.promptTemplates ?? (await discoverPromptTemplates(cwd, agentDir));
1073
- time("discoverPromptTemplates");
1063
+ const [systemPrompt, promptTemplates, slashCommands, customCommandsResult] = await Promise.all([
1064
+ rebuildSystemPrompt(initialToolNames, toolRegistry),
1065
+ options.promptTemplates ?? discoverPromptTemplates(cwd, agentDir),
1066
+ options.slashCommands ?? discoverSlashCommands(cwd),
1067
+ customCommandsPromise,
1068
+ ]);
1069
+ time("buildSystemPrompt+discoverPromptTemplates+discoverSlashCommands");
1074
1070
  toolSession.promptTemplates = promptTemplates;
1075
1071
 
1076
- const slashCommands = options.slashCommands ?? (await discoverSlashCommands(cwd));
1077
- time("discoverSlashCommands");
1078
-
1079
1072
  // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
1080
1073
  const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
1081
1074
  const converted = convertToLlm(messages);
@@ -115,7 +115,6 @@ import {
115
115
  maybeAbortStreamingEdit,
116
116
  preCacheStreamingEditFile,
117
117
  resetStreamingEditState,
118
- rewriteToolCallArgs,
119
118
  } from "./streaming-edit";
120
119
  import {
121
120
  addPendingTtsrInjections,
@@ -523,17 +522,12 @@ export class AgentSession {
523
522
  }
524
523
 
525
524
  if (event.message.role === "toolResult") {
526
- const { toolName, $normative, toolCallId, details, isError, content } = event.message as {
525
+ const { toolName, details, isError, content } = event.message as {
527
526
  toolName?: string;
528
- toolCallId?: string;
529
527
  details?: { path?: string };
530
- $normative?: Record<string, unknown>;
531
528
  isError?: boolean;
532
529
  content?: Array<TextContent | ImageContent>;
533
530
  };
534
- if ($normative && toolCallId && this.settings.get("normativeRewrite")) {
535
- await rewriteToolCallArgs(this.agent, this.sessionManager, toolCallId, $normative);
536
- }
537
531
  // Invalidate streaming edit cache when edit tool completes to prevent stale data
538
532
  if (toolName === "edit" && details?.path) {
539
533
  invalidateFileCacheForPath(this.#streamingEdit, details.path, this.sessionManager.getCwd());
@@ -1581,36 +1581,6 @@ export class SessionManager {
1581
1581
  await this.#rewriteFile();
1582
1582
  }
1583
1583
 
1584
- /**
1585
- * Rewrite tool call arguments in the most recent assistant message containing the toolCallId.
1586
- * Returns true if a tool call was updated.
1587
- */
1588
- async rewriteAssistantToolCallArgs(toolCallId: string, args: Record<string, unknown>): Promise<boolean> {
1589
- let updated = false;
1590
- for (let i = this.#fileEntries.length - 1; i >= 0; i--) {
1591
- const entry = this.#fileEntries[i];
1592
- if (entry.type !== "message" || entry.message.role !== "assistant") continue;
1593
- const message = entry.message as { content?: unknown };
1594
- if (!Array.isArray(message.content)) continue;
1595
- for (const block of message.content) {
1596
- if (typeof block !== "object" || block === null) continue;
1597
- if (!("type" in block) || (block as { type?: string }).type !== "toolCall") continue;
1598
- const toolCall = block as { id?: string; arguments?: Record<string, unknown> };
1599
- if (toolCall.id === toolCallId) {
1600
- toolCall.arguments = args;
1601
- updated = true;
1602
- break;
1603
- }
1604
- }
1605
- if (updated) break;
1606
- }
1607
-
1608
- if (updated && this.persist && this.#sessionFile) {
1609
- await this.#rewriteFile();
1610
- }
1611
- return updated;
1612
- }
1613
-
1614
1584
  /**
1615
1585
  * Append a custom message entry (for extensions) that participates in LLM context.
1616
1586
  * @param customType Hook identifier for filtering on reload
@@ -1,13 +1,12 @@
1
1
  import * as fs from "node:fs";
2
2
 
3
3
  import type { Agent, AgentEvent } from "@nghyane/arcane-agent";
4
- import type { AssistantMessage, ToolCall } from "@nghyane/arcane-ai";
4
+ import type { ToolCall } from "@nghyane/arcane-ai";
5
5
  import { isEnoent, logger } from "@nghyane/arcane-utils";
6
6
  import type { Settings } from "../config/settings";
7
7
  import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
8
8
  import type { SecretObfuscator } from "../secrets/obfuscator";
9
9
  import { resolveToCwd } from "../tools/path-utils";
10
- import type { SessionManager } from "./session-manager";
11
10
 
12
11
  /**
13
12
  * Mutable state for streaming edit abort detection.
@@ -222,37 +221,3 @@ async function checkPreviewPatchAsync(
222
221
  agent.abort();
223
222
  }
224
223
  }
225
-
226
- /**
227
- * Rewrite tool call arguments in agent state and persisted session history.
228
- */
229
- export async function rewriteToolCallArgs(
230
- agent: Agent,
231
- sessionManager: SessionManager,
232
- toolCallId: string,
233
- args: Record<string, unknown>,
234
- ): Promise<void> {
235
- let updated = false;
236
- const messages = agent.state.messages;
237
- for (let i = messages.length - 1; i >= 0; i--) {
238
- const msg = messages[i];
239
- if (msg.role !== "assistant") continue;
240
- const assistantMsg = msg as AssistantMessage;
241
- if (!Array.isArray(assistantMsg.content)) continue;
242
- for (const block of assistantMsg.content) {
243
- if (typeof block !== "object" || block === null) continue;
244
- if (!("type" in block) || (block as { type?: string }).type !== "toolCall") continue;
245
- const toolCall = block as { id?: string; arguments?: Record<string, unknown> };
246
- if (toolCall.id === toolCallId) {
247
- toolCall.arguments = args;
248
- updated = true;
249
- break;
250
- }
251
- }
252
- if (updated) break;
253
- }
254
-
255
- if (updated) {
256
- await sessionManager.rewriteAssistantToolCallArgs(toolCallId, args);
257
- }
258
- }
package/src/tools/bash.ts CHANGED
@@ -55,7 +55,8 @@ function isInteractiveResult(result: BashResult | BashInteractiveResult): result
55
55
  export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails, Theme> {
56
56
  readonly name = "bash";
57
57
  readonly label = "Bash";
58
- description = "Execute a shell command";
58
+ description =
59
+ "Execute a shell command. Use grep/find instead of shell grep/find, read instead of cat/head/tail, edit instead of sed/awk, write instead of echo/printf redirects.";
59
60
  readonly parameters = bashSchema;
60
61
  readonly concurrency = "exclusive";
61
62
 
@@ -1,7 +1,5 @@
1
1
  import type { AgentTool } from "@nghyane/arcane-agent";
2
- import { $env, logger } from "@nghyane/arcane-utils";
3
- import { getPreludeDocs, warmPythonEnvironment } from "../ipy/executor";
4
- import { checkPythonKernelAvailability } from "../ipy/kernel";
2
+ import { $env } from "@nghyane/arcane-utils";
5
3
  import { LspTool } from "../lsp";
6
4
  import { EditTool } from "../patch";
7
5
  import { TaskTool } from "../task";
@@ -106,37 +104,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
106
104
  const enableLsp = session.enableLsp ?? true;
107
105
  const requestedTools = toolNames && toolNames.length > 0 ? [...new Set(toolNames)] : undefined;
108
106
  const pythonMode = getPythonModeFromEnv() ?? session.settings.get("python.toolMode");
109
- const skipPythonPreflight = session.skipPythonPreflight === true;
110
- let pythonAvailable = true;
111
- const shouldCheckPython =
112
- !skipPythonPreflight &&
113
- pythonMode !== "bash-only" &&
114
- (requestedTools === undefined || requestedTools.includes("python"));
115
- const isTestEnv = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
116
- const skipPythonWarm = isTestEnv || $env.ARCANE_PYTHON_SKIP_CHECK === "1";
117
- if (shouldCheckPython) {
118
- const availability = await checkPythonKernelAvailability(session.cwd);
119
- time("createTools:pythonCheck");
120
- pythonAvailable = availability.ok;
121
- if (!availability.ok) {
122
- logger.warn("Python kernel unavailable, falling back to bash", {
123
- reason: availability.reason,
124
- });
125
- } else if (!skipPythonWarm && getPreludeDocs().length === 0) {
126
- const sessionFile = session.getSessionFile?.() ?? undefined;
127
- const warmSessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
128
- try {
129
- await warmPythonEnvironment(session.cwd, warmSessionId, session.settings.get("python.sharedGateway"));
130
- time("createTools:warmPython");
131
- } catch (err) {
132
- logger.warn("Failed to warm Python environment", {
133
- error: err instanceof Error ? err.message : String(err),
134
- });
135
- }
136
- }
137
- }
138
107
 
139
- const effectiveMode = pythonAvailable ? pythonMode : "bash-only";
108
+ const effectiveMode = pythonMode;
140
109
  const allowBash = effectiveMode !== "ipy-only";
141
110
  const allowPython = effectiveMode !== "bash-only";
142
111
  if (
@@ -850,7 +850,7 @@ export interface FetchToolDetails {
850
850
  export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails, Theme> {
851
851
  readonly name = "fetch";
852
852
  readonly label = "Fetch";
853
- description = "Fetch a URL and return its content";
853
+ description = "Fetch a URL and return its content. Do NOT use for localhost or local URLs; use bash curl instead.";
854
854
  readonly parameters = fetchSchema;
855
855
 
856
856
  constructor(private readonly session: ToolSession) {}
package/src/tools/grep.ts CHANGED
@@ -54,7 +54,8 @@ type GrepParams = Static<typeof grepSchema>;
54
54
  export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails, Theme> {
55
55
  readonly name = "grep";
56
56
  readonly label = "Grep";
57
- description = "Search file contents with regex";
57
+ description =
58
+ "Search file contents with regex. Use for exact text matches (variable names, function calls, strings). Use explore for semantic/conceptual searches.";
58
59
  readonly parameters = grepSchema;
59
60
 
60
61
  constructor(private readonly session: ToolSession) {}
@@ -4,11 +4,13 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
4
4
  import type { ImageContent } from "@nghyane/arcane-ai";
5
5
  import type { Component } from "@nghyane/arcane-tui";
6
6
  import { Text } from "@nghyane/arcane-tui";
7
+ import { $env, logger } from "@nghyane/arcane-utils";
7
8
  import { getProjectDir } from "@nghyane/arcane-utils/dirs";
8
9
  import { type Static, Type } from "@sinclair/typebox";
9
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
- import { executePython, type PythonExecutorOptions } from "../ipy/executor";
11
+ import { executePython, getPreludeDocs, type PythonExecutorOptions, warmPythonEnvironment } from "../ipy/executor";
11
12
  import type { PythonStatusEvent } from "../ipy/kernel";
13
+ import { checkPythonKernelAvailability } from "../ipy/kernel";
12
14
  import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary } from "../session/streaming-output";
13
15
  import type { Theme } from "../theme/theme";
14
16
  import { renderCodeCell, renderStatusLine } from "../tui";
@@ -76,6 +78,8 @@ export class PythonTool implements AgentTool<typeof pythonSchema, any, Theme> {
76
78
  readonly concurrency = "exclusive";
77
79
 
78
80
  readonly #proxyExecutor?: PythonProxyExecutor;
81
+ #initialized = false;
82
+ #initPromise: Promise<void> | undefined;
79
83
 
80
84
  constructor(
81
85
  private readonly session: ToolSession | null,
@@ -84,6 +88,52 @@ export class PythonTool implements AgentTool<typeof pythonSchema, any, Theme> {
84
88
  this.#proxyExecutor = options?.proxyExecutor;
85
89
  }
86
90
 
91
+ async #ensureInitialized(): Promise<void> {
92
+ if (this.#initialized) return;
93
+ if (this.#initPromise) return this.#initPromise;
94
+ this.#initPromise = this.#doInit();
95
+ return this.#initPromise;
96
+ }
97
+
98
+ async #doInit(): Promise<void> {
99
+ try {
100
+ const isTestEnv = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
101
+ if (isTestEnv || $env.ARCANE_PYTHON_SKIP_CHECK === "1") {
102
+ this.#initialized = true;
103
+ return;
104
+ }
105
+ if (!this.session) {
106
+ this.#initialized = true;
107
+ return;
108
+ }
109
+ const availability = await checkPythonKernelAvailability(this.session.cwd);
110
+ if (!availability.ok) {
111
+ throw new ToolError(`Python kernel unavailable: ${availability.reason}`);
112
+ }
113
+ if (getPreludeDocs().length === 0) {
114
+ const sessionFile = this.session.getSessionFile?.() ?? undefined;
115
+ const warmSessionId = sessionFile
116
+ ? `session:${sessionFile}:cwd:${this.session.cwd}`
117
+ : `cwd:${this.session.cwd}`;
118
+ try {
119
+ await warmPythonEnvironment(
120
+ this.session.cwd,
121
+ warmSessionId,
122
+ this.session.settings.get("python.sharedGateway"),
123
+ );
124
+ } catch (err) {
125
+ logger.warn("Failed to warm Python environment", {
126
+ error: err instanceof Error ? err.message : String(err),
127
+ });
128
+ }
129
+ }
130
+ this.#initialized = true;
131
+ } catch (err) {
132
+ this.#initPromise = undefined;
133
+ throw err;
134
+ }
135
+ }
136
+
87
137
  async execute(
88
138
  _toolCallId: string,
89
139
  params: Static<typeof pythonSchema>,
@@ -99,6 +149,8 @@ export class PythonTool implements AgentTool<typeof pythonSchema, any, Theme> {
99
149
  throw new ToolError("Python tool requires a session when not using proxy executor");
100
150
  }
101
151
 
152
+ await this.#ensureInitialized();
153
+
102
154
  const { cells, timeout: rawTimeout = 30, cwd, reset } = params;
103
155
  // Clamp to reasonable range: 1s - 600s (10 min)
104
156
  const timeoutSec = Math.max(1, Math.min(600, rawTimeout));
package/src/tools/read.ts CHANGED
@@ -533,7 +533,8 @@ type ReadParams = ReadToolInput;
533
533
  export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails, Theme> {
534
534
  readonly name = "read";
535
535
  readonly label = "Read";
536
- description = "Read file contents, list directories, or view images";
536
+ description =
537
+ "Read file contents, list directories, or view images. When possible, call in parallel for all files you need. Avoid tiny repeated slices — read a larger range instead.";
537
538
  readonly parameters = readSchema;
538
539
  readonly nonAbortable = true;
539
540
 
@@ -71,7 +71,7 @@ type WriteParams = WriteToolInput;
71
71
  export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails, Theme> {
72
72
  readonly name = "write";
73
73
  readonly label = "Write";
74
- description = "Create a new file";
74
+ description = "Create a new file. For existing files, prefer edit instead — even for extensive changes.";
75
75
  readonly parameters = writeSchema;
76
76
  readonly nonAbortable = true;
77
77
  readonly concurrency = "exclusive";
@@ -265,7 +265,8 @@ export async function runSearchQuery(params: SearchParams): Promise<{
265
265
  export class SearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails, Theme> {
266
266
  readonly name = "web_search";
267
267
  readonly label = "Web Search";
268
- readonly description = "Search the web";
268
+ readonly description =
269
+ "Search the web for up-to-date information. Use fetch to read full content from a specific URL.";
269
270
  readonly parameters = webSearchSchema;
270
271
  readonly renderCall = renderSearchCall;
271
272
  readonly renderResult = renderSearchResult;
@@ -1,72 +0,0 @@
1
- /**
2
- * Normalize applied patch output into a canonical edit tool payload.
3
- */
4
- import { generateUnifiedDiffString } from "./diff";
5
- import { normalizeToLF, stripBom } from "./normalize";
6
- import { parseHunks } from "./parser";
7
- import type { PatchInput } from "./types";
8
-
9
- export interface NormativePatchOptions {
10
- path: string;
11
- rename?: string;
12
- oldContent: string;
13
- newContent: string;
14
- contextLines?: number;
15
- anchor?: string | string[];
16
- }
17
-
18
- /** Normative patch input is the MongoDB-style update variant */
19
-
20
- function applyAnchors(diff: string, anchors: Array<string | undefined> | undefined): string {
21
- if (!anchors || anchors.length === 0) {
22
- return diff;
23
- }
24
- const lines = diff.split("\n");
25
- let anchorIndex = 0;
26
- for (let i = 0; i < lines.length; i++) {
27
- if (!lines[i].startsWith("@@")) continue;
28
- const anchor = anchors[anchorIndex];
29
- if (anchor !== undefined) {
30
- lines[i] = anchor.trim().length === 0 ? "@@" : `@@ ${anchor}`;
31
- }
32
- anchorIndex++;
33
- }
34
- return lines.join("\n");
35
- }
36
-
37
- function deriveAnchors(diff: string): Array<string | undefined> {
38
- const hunks = parseHunks(diff);
39
- return hunks.map(hunk => {
40
- if (hunk.oldLines.length === 0 || hunk.newLines.length === 0) {
41
- return undefined;
42
- }
43
- const newLines = new Set(hunk.newLines);
44
- for (const line of hunk.oldLines) {
45
- const trimmed = line.trim();
46
- if (trimmed.length === 0) continue;
47
- if (!/[A-Za-z0-9_]/.test(trimmed)) continue;
48
- if (newLines.has(line)) {
49
- return trimmed;
50
- }
51
- }
52
- return undefined;
53
- });
54
- }
55
-
56
- export function buildNormativeUpdateInput(options: NormativePatchOptions): PatchInput {
57
- const normalizedOld = normalizeToLF(stripBom(options.oldContent).text);
58
- const normalizedNew = normalizeToLF(stripBom(options.newContent).text);
59
- const diffResult = generateUnifiedDiffString(normalizedOld, normalizedNew, options.contextLines ?? 3);
60
- let anchors: Array<string | undefined> | undefined =
61
- typeof options.anchor === "string" ? [options.anchor] : options.anchor;
62
- if (!anchors) {
63
- anchors = deriveAnchors(diffResult.diff);
64
- }
65
- const diff = applyAnchors(diffResult.diff, anchors);
66
- return {
67
- path: options.path,
68
- op: "update",
69
- rename: options.rename,
70
- diff,
71
- };
72
- }