@nghyane/arcane 0.1.15 → 0.1.17

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 (45) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +7 -15
  3. package/src/config/keybindings.ts +9 -7
  4. package/src/config/settings-schema.ts +19 -46
  5. package/src/config/settings.ts +0 -1
  6. package/src/exa/mcp-client.ts +57 -2
  7. package/src/internal-urls/docs-index.generated.ts +1 -2
  8. package/src/internal-urls/index.ts +2 -4
  9. package/src/internal-urls/router.ts +2 -2
  10. package/src/internal-urls/types.ts +2 -2
  11. package/src/mcp/oauth-flow.ts +1 -1
  12. package/src/modes/controllers/command-controller.ts +26 -64
  13. package/src/modes/utils/ui-helpers.ts +2 -1
  14. package/src/patch/hashline.ts +42 -0
  15. package/src/prompts/system/system-prompt.md +14 -10
  16. package/src/prompts/thread-extract.md +16 -0
  17. package/src/prompts/tools/render-mermaid.md +9 -0
  18. package/src/sdk.ts +1 -19
  19. package/src/session/agent-session.ts +4 -3
  20. package/src/session/retry-utils.ts +1 -1
  21. package/src/session/session-index.ts +329 -0
  22. package/src/slash-commands/builtin-registry.ts +0 -16
  23. package/src/task/index.ts +1 -1
  24. package/src/tools/ask.ts +9 -6
  25. package/src/tools/bash-skill-urls.ts +3 -3
  26. package/src/tools/create-tools.ts +26 -0
  27. package/src/tools/find-thread.ts +120 -0
  28. package/src/tools/index.ts +5 -0
  29. package/src/tools/read-thread.ts +409 -0
  30. package/src/tools/read.ts +2 -2
  31. package/src/tools/render-mermaid.ts +68 -0
  32. package/src/tools/save-memory.ts +182 -0
  33. package/src/web/search/index.ts +2 -0
  34. package/src/web/search/provider.ts +3 -0
  35. package/src/web/search/providers/anthropic.ts +1 -0
  36. package/src/web/search/providers/gemini.ts +122 -37
  37. package/src/web/search/providers/kagi.ts +163 -0
  38. package/src/web/search/types.ts +1 -0
  39. package/src/internal-urls/memory-protocol.ts +0 -133
  40. package/src/memories/index.ts +0 -1099
  41. package/src/memories/storage.ts +0 -563
  42. package/src/prompts/memories/consolidation.md +0 -30
  43. package/src/prompts/memories/read_path.md +0 -11
  44. package/src/prompts/memories/stage_one_input.md +0 -6
  45. package/src/prompts/memories/stage_one_system.md +0 -21
@@ -14,10 +14,10 @@ import { Snowflake } from "@nghyane/arcane-utils";
14
14
  import { setProjectDir } from "@nghyane/arcane-utils/dirs";
15
15
  import { $ } from "bun";
16
16
  import { reset as resetCapabilities } from "../../capability";
17
+ import { formatKeyHint, type KeyId } from "../../config/keybindings";
17
18
  import { loadCustomShare } from "../../export/custom-share";
18
19
  import type { CompactOptions } from "../../extensibility/extensions/types";
19
20
  import { getGatewayStatus } from "../../ipy/gateway-coordinator";
20
- import { buildMemoryToolDeveloperInstructions, clearMemoryData, enqueueMemoryConsolidation } from "../../memories";
21
21
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
22
22
  import { BorderedLoader } from "../../modes/components/bordered-loader";
23
23
  import { DynamicBorder } from "../../modes/components/dynamic-border";
@@ -362,43 +362,44 @@ export class CommandController {
362
362
  }
363
363
 
364
364
  handleHotkeysCommand(): void {
365
- const expandToolsKey = this.ctx.keybindings.getDisplayString("expandTools") || "Ctrl+O";
366
- const sttKey = this.ctx.keybindings.getDisplayString("toggleSTT") || "Alt+H";
365
+ const k = (id: string) => formatKeyHint(id as KeyId);
366
+ const expandToolsKey = this.ctx.keybindings.getDisplayString("expandTools") || k("ctrl+o");
367
+ const sttKey = this.ctx.keybindings.getDisplayString("toggleSTT") || k("alt+h");
367
368
  const hotkeys = `
368
369
  **Navigation**
369
370
  | Key | Action |
370
371
  |-----|--------|
371
372
  | \`Arrow keys\` | Move cursor / browse history (Up when empty) |
372
- | \`Option+Left/Right\` | Move by word |
373
- | \`Ctrl+A\` / \`Home\` / \`Cmd+Left\` | Start of line |
374
- | \`Ctrl+E\` / \`End\` / \`Cmd+Right\` | End of line |
373
+ | \`${k("alt+left")}/${k("alt+right")}\` | Move by word |
374
+ | \`${k("ctrl+a")}\` / \`Home\` | Start of line |
375
+ | \`${k("ctrl+e")}\` / \`End\` | End of line |
375
376
 
376
377
  **Editing**
377
378
  | Key | Action |
378
379
  |-----|--------|
379
380
  | \`Enter\` | Send message |
380
- | \`Shift+Enter\` / \`Alt+Enter\` | New line |
381
- | \`Ctrl+W\` / \`Option+Backspace\` | Delete word backwards |
382
- | \`Ctrl+U\` | Delete to start of line |
383
- | \`Ctrl+K\` | Delete to end of line |
381
+ | \`${k("shift+enter")}\` / \`${k("alt+enter")}\` | New line |
382
+ | \`${k("ctrl+w")}\` / \`${k("alt+backspace")}\` | Delete word backwards |
383
+ | \`${k("ctrl+u")}\` | Delete to start of line |
384
+ | \`${k("ctrl+k")}\` | Delete to end of line |
384
385
 
385
386
  **Other**
386
387
  | Key | Action |
387
388
  |-----|--------|
388
389
  | \`Tab\` | Path completion / accept autocomplete |
389
390
  | \`Escape\` | Cancel autocomplete / abort streaming |
390
- | \`Ctrl+C\` | Clear editor (first) / exit (second) |
391
- | \`Ctrl+D\` | Exit (when editor is empty) |
392
- | \`Ctrl+Z\` | Suspend to background |
393
- | \`Shift+Tab\` | Cycle thinking level |
394
- | \`Ctrl+P\` | Cycle role models (slow/default/fast) |
395
- | \`Shift+Ctrl+P\` | Cycle role models (temporary) |
396
- | \`Alt+P\` | Select model (temporary) |
397
- | \`Ctrl+L\` | Select model (set roles) |
398
- | \`Ctrl+R\` | Search prompt history |
391
+ | \`${k("ctrl+c")}\` | Clear editor (first) / exit (second) |
392
+ | \`${k("ctrl+d")}\` | Exit (when editor is empty) |
393
+ | \`${k("ctrl+z")}\` | Suspend to background |
394
+ | \`${k("shift+tab")}\` | Cycle thinking level |
395
+ | \`${k("ctrl+p")}\` | Cycle role models (slow/default/fast) |
396
+ | \`${k("shift+ctrl+p")}\` | Cycle role models (temporary) |
397
+ | \`${k("alt+p")}\` | Select model (temporary) |
398
+ | \`${k("ctrl+l")}\` | Select model (set roles) |
399
+ | \`${k("ctrl+r")}\` | Search prompt history |
399
400
  | \`${expandToolsKey}\` | Toggle tool output expansion |
400
- | \`Ctrl+T\` | Toggle todo list expansion |
401
- | \`Ctrl+G\` | Edit message in external editor |
401
+ | \`${k("ctrl+t")}\` | Toggle todo list expansion |
402
+ | \`${k("ctrl+g")}\` | Edit message in external editor |
402
403
  | \`${sttKey}\` | Toggle speech-to-text recording |
403
404
  | \`/\` | Slash commands |
404
405
  | \`!\` | Run bash command |
@@ -415,49 +416,10 @@ export class CommandController {
415
416
  this.ctx.ui.requestRender();
416
417
  }
417
418
 
418
- async handleMemoryCommand(text: string): Promise<void> {
419
- const argumentText = text.slice(7).trim();
420
- const action = argumentText.split(/\s+/, 1)[0]?.toLowerCase() || "view";
421
- const agentDir = this.ctx.settings.getAgentDir();
422
-
423
- if (action === "view") {
424
- const payload = await buildMemoryToolDeveloperInstructions(agentDir, this.ctx.settings);
425
- if (!payload) {
426
- this.ctx.showWarning("Memory payload is empty (memories disabled or no memory summary found).");
427
- return;
428
- }
429
- this.ctx.chatContainer.addChild(new Spacer(1));
430
- this.ctx.chatContainer.addChild(new DynamicBorder());
431
- this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Memory Injection Payload")), 1, 0));
432
- this.ctx.chatContainer.addChild(new Spacer(1));
433
- this.ctx.chatContainer.addChild(new Markdown(payload, 1, 1, getMarkdownTheme()));
434
- this.ctx.chatContainer.addChild(new DynamicBorder());
435
- this.ctx.ui.requestRender();
436
- return;
437
- }
438
-
439
- if (action === "reset" || action === "clear") {
440
- try {
441
- await clearMemoryData(agentDir, this.ctx.sessionManager.getCwd());
442
- await this.ctx.session.refreshBaseSystemPrompt();
443
- this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
444
- } catch (error) {
445
- this.ctx.showError(`Memory clear failed: ${error instanceof Error ? error.message : String(error)}`);
446
- }
447
- return;
448
- }
449
-
450
- if (action === "enqueue" || action === "rebuild") {
451
- try {
452
- enqueueMemoryConsolidation(agentDir);
453
- this.ctx.showStatus("Memory consolidation enqueued.");
454
- } catch (error) {
455
- this.ctx.showError(`Memory enqueue failed: ${error instanceof Error ? error.message : String(error)}`);
456
- }
457
- return;
458
- }
459
-
460
- this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild>");
419
+ async handleMemoryCommand(_text: string): Promise<void> {
420
+ this.ctx.showWarning(
421
+ "The /memory command has been removed. Use save_memory tool or add facts to AGENTS.md directly.",
422
+ );
461
423
  }
462
424
 
463
425
  async handleClearCommand(): Promise<void> {
@@ -1,6 +1,7 @@
1
1
  import type { AgentMessage } from "@nghyane/arcane-agent";
2
2
  import type { AssistantMessage, Message } from "@nghyane/arcane-ai";
3
3
  import { Spacer, Text, TruncatedText } from "@nghyane/arcane-tui";
4
+ import { formatKeyHint, type KeyId } from "../../config/keybindings";
4
5
  import { settings } from "../../config/settings";
5
6
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
6
7
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
@@ -385,7 +386,7 @@ export class UiHelpers {
385
386
  const queuedText = theme.fg("dim", `${entry.label}: ${entry.message}`);
386
387
  this.ctx.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
387
388
  }
388
- const dequeueKey = this.ctx.keybindings.getDisplayString("dequeue") || "Alt+Up";
389
+ const dequeueKey = this.ctx.keybindings.getDisplayString("dequeue") || formatKeyHint("alt+up" as KeyId);
389
390
  const hintText = theme.fg("dim", `${theme.tree.hook} ${dequeueKey} to edit`);
390
391
  this.ctx.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
391
392
  }
@@ -566,6 +566,37 @@ export function validateLineRef(ref: { line: number; hash: string }, fileLines:
566
566
  // Edit Application
567
567
  // ═══════════════════════════════════════════════════════════════════════════
568
568
 
569
+ /**
570
+ * Detect suspicious Unicode escape placeholders in edit lines.
571
+ * LLMs sometimes emit literal `\uDDDD` strings instead of actual Unicode characters.
572
+ * Returns a warning message if detected, undefined otherwise.
573
+ */
574
+ function detectUnicodeEscapePlaceholders(lines: string[]): string | undefined {
575
+ for (const line of lines) {
576
+ if (/\\u[0-9A-Fa-f]{4}/.test(line)) {
577
+ return "Warning: edit content contains literal Unicode escape sequences (\\uXXXX). These may be intended as actual Unicode characters.";
578
+ }
579
+ }
580
+ return undefined;
581
+ }
582
+
583
+ /**
584
+ * Auto-correct escaped tab indentation in edit lines.
585
+ * When enabled via ARCANE_HASHLINE_AUTOCORRECT_ESCAPED_TABS=1, replaces
586
+ * leading `\\t` sequences (literal backslash-t from JSON) with real tab characters.
587
+ */
588
+ function autocorrectEscapedTabs(lines: string[]): string[] {
589
+ if (Bun.env.ARCANE_HASHLINE_AUTOCORRECT_ESCAPED_TABS !== "1") {
590
+ return lines;
591
+ }
592
+ return lines.map(line => {
593
+ const match = line.match(/^((?:\\t)+)/);
594
+ if (!match) return line;
595
+ const tabCount = match[1].length / 2; // each \\t is 2 chars
596
+ return "\t".repeat(tabCount) + line.slice(match[1].length);
597
+ });
598
+ }
599
+
569
600
  /**
570
601
  * Apply an array of hashline edits to file content.
571
602
  *
@@ -599,6 +630,16 @@ export function applyHashlineEdits(
599
630
 
600
631
  const autocorrect = Bun.env.ARCANE_HL_AUTOCORRECT === "1";
601
632
 
633
+ // Collect warnings and auto-correct edit content
634
+ const warnings: string[] = [];
635
+ for (const edit of edits) {
636
+ const unicodeWarning = detectUnicodeEscapePlaceholders(edit.content);
637
+ if (unicodeWarning && !warnings.includes(unicodeWarning)) {
638
+ warnings.push(unicodeWarning);
639
+ }
640
+ edit.content = autocorrectEscapedTabs(edit.content);
641
+ }
642
+
602
643
  function collectExplicitlyTouchedLines(): Set<number> {
603
644
  const touched = new Set<number>();
604
645
  for (const edit of edits) {
@@ -914,6 +955,7 @@ export function applyHashlineEdits(
914
955
  return {
915
956
  content: finalContent,
916
957
  firstChangedLine,
958
+ ...(warnings.length > 0 ? { warnings } : {}),
917
959
  ...(noopEdits.length > 0 ? { noopEdits } : {}),
918
960
  };
919
961
 
@@ -162,6 +162,20 @@ Best practices:
162
162
  - Run multiple sub-agents concurrently if tasks are independent with disjoint write targets.
163
163
  {{/has}}
164
164
 
165
+ ### Cross-session Knowledge
166
+
167
+ Tools: `find_thread`, `read_thread`, `save_memory`
168
+ **Proactive search triggers** — use `find_thread` when:
169
+ - User mentions past work: "we did this before", "last time", "in a previous session"
170
+ - User asks "what did we do about X" or "how did we solve Y"
171
+ - Task seems related to work that may have been done before
172
+ - Handoff context references a parent thread and you need more detail
173
+ **Do NOT search when:**
174
+ - Question is about current session context
175
+ - Generic coding question with no project-specific history
176
+ - User explicitly provides all needed context
177
+ **save_memory**: only when user says "remember this" or states a clear preference. If unsure, ask.
178
+
165
179
  ### Verification
166
180
  After completing changes, verify using commands from AGENTS.md or the project's config. Format → typecheck/lint → test (if relevant) → build (if required).
167
181
  Report evidence concisely: counts, pass/fail, error summary.
@@ -252,16 +266,6 @@ Scan descriptions vs task domain — read skill if ≥50% likely relevant.
252
266
  </rules>
253
267
  {{/if}}
254
268
 
255
- {{#if memories.length}}
256
- <memories>
257
- {{#each memories}}
258
- <memory path="{{path}}">
259
- {{content}}
260
- </memory>
261
- {{/each}}
262
- </memories>
263
- {{/if}}
264
-
265
269
  {{#if preloadedSkills.length}}
266
270
  {{#each preloadedSkills}}
267
271
  <skill name="{{name}}">
@@ -0,0 +1,16 @@
1
+ You are helping extract relevant information from a conversation thread based on a goal.
2
+
3
+ ## Task
4
+
5
+ I am providing a conversation thread rendered as markdown, along with a goal describing what information to extract.
6
+
7
+ Your job is to:
8
+ 1. Analyze the thread content
9
+ 2. Identify information that is relevant to the goal
10
+ 3. Extract and preserve those relevant parts with full fidelity
11
+
12
+ ## Rules
13
+ - Be concise but complete — include all relevant details
14
+ - Preserve code snippets, file paths, commands, and decisions exactly as they appear
15
+ - Omit pleasantries, failed attempts, and thinking-out-loud unless the goal asks for them
16
+ - If nothing relevant is found, say so briefly
@@ -0,0 +1,9 @@
1
+ Convert Mermaid graph source into ASCII diagram output.
2
+
3
+ Parameters:
4
+ - `mermaid` (required): Mermaid graph text to render.
5
+ - `config` (optional): JSON render configuration (spacing and layout options).
6
+ Behavior:
7
+ - Returns ASCII diagram text.
8
+ - Saves full ASCII output to an artifact URL (`artifact://<id>`) when artifact storage is available.
9
+ - Returns an error when the Mermaid input is invalid or rendering fails.
package/src/sdk.ts CHANGED
@@ -42,13 +42,11 @@ import {
42
42
  ArtifactProtocolHandler,
43
43
  DocsProtocolHandler,
44
44
  InternalUrlRouter,
45
- MemoryProtocolHandler,
46
45
  RuleProtocolHandler,
47
46
  SkillProtocolHandler,
48
47
  } from "./internal-urls";
49
48
  import { disposeAllKernelSessions } from "./ipy/executor";
50
49
  import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
51
- import { buildMemoryToolDeveloperInstructions, getMemoryRoot, startMemoryStartupTask } from "./memories";
52
50
  import { collectEnvSecrets, loadSecrets, obfuscateMessages, SecretObfuscator } from "./secrets";
53
51
  import { AgentSession } from "./session/agent-session";
54
52
  import { AuthStorage } from "./session/auth-storage";
@@ -735,7 +733,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
735
733
  settings,
736
734
  };
737
735
 
738
- // Initialize internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://)
736
+ // Initialize internal URL router for internal protocols (agent://, artifact://, skill://, rule://)
739
737
  const internalRouter = new InternalUrlRouter();
740
738
  const getArtifactsDir = () => {
741
739
  const sessionFile = sessionManager.getSessionFile();
@@ -743,11 +741,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
743
741
  };
744
742
  internalRouter.register(new AgentProtocolHandler({ getArtifactsDir }));
745
743
  internalRouter.register(new ArtifactProtocolHandler({ getArtifactsDir }));
746
- internalRouter.register(
747
- new MemoryProtocolHandler({
748
- getMemoryRoot: () => getMemoryRoot(agentDir, settings.getCwd()),
749
- }),
750
- );
751
744
  internalRouter.register(
752
745
  new SkillProtocolHandler({
753
746
  getSkills: () => skills,
@@ -1027,7 +1020,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1027
1020
 
1028
1021
  const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
1029
1022
  toolContextStore.setToolNames(toolNames);
1030
- const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
1031
1023
  const defaultPrompt = await buildSystemPromptInternal({
1032
1024
  cwd,
1033
1025
  skills,
@@ -1037,7 +1029,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1037
1029
  toolNames,
1038
1030
  rules: rulebookRules,
1039
1031
  skillsSettings: settings.getGroup("skills") as SkillsSettings,
1040
- appendSystemPrompt: memoryInstructions,
1041
1032
  });
1042
1033
 
1043
1034
  if (options.systemPrompt === undefined) {
@@ -1054,7 +1045,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1054
1045
  rules: rulebookRules,
1055
1046
  skillsSettings: settings.getGroup("skills") as SkillsSettings,
1056
1047
  customPrompt: options.systemPrompt,
1057
- appendSystemPrompt: memoryInstructions,
1058
1048
  });
1059
1049
  }
1060
1050
  return options.systemPrompt(defaultPrompt);
@@ -1258,14 +1248,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1258
1248
  }
1259
1249
  }
1260
1250
 
1261
- startMemoryStartupTask({
1262
- session,
1263
- settings,
1264
- modelRegistry,
1265
- agentDir,
1266
- isSubagent,
1267
- });
1268
-
1269
1251
  return {
1270
1252
  session,
1271
1253
  extensionsResult,
@@ -2067,9 +2067,10 @@ Be thorough - include exact file paths, function names, error messages, and tech
2067
2067
  return undefined;
2068
2068
  }
2069
2069
 
2070
- // Start a new session
2070
+ // Start a new session with parent reference
2071
+ const parentThreadId = this.sessionManager.getSessionId();
2071
2072
  await this.sessionManager.flush();
2072
- await this.sessionManager.newSession();
2073
+ await this.sessionManager.newSession({ parentSession: parentThreadId });
2073
2074
  this.agent.reset();
2074
2075
  this.agent.sessionId = this.sessionManager.getSessionId();
2075
2076
  this.#steeringMessages = [];
@@ -2078,7 +2079,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
2078
2079
  this.#todoReminderCount = 0;
2079
2080
 
2080
2081
  // Inject the handoff document as a custom message
2081
- const handoffContent = `<handoff-context>\n${handoffText}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
2082
+ const handoffContent = `<handoff-context thread="${parentThreadId}">\n${handoffText}\n</handoff-context>\n\nThe above is a handoff document from thread \`${parentThreadId}\`. Use this context to continue the work seamlessly. If you need additional details not covered above, use \`read_thread("${parentThreadId}", "your specific question")\` to query the original session.`;
2082
2083
  this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true);
2083
2084
 
2084
2085
  // Rebuild agent messages from session
@@ -17,7 +17,7 @@ export function isRetryableErrorMessage(errorMessage: string): boolean {
17
17
  * Check if an error message indicates a usage/billing limit (non-transient).
18
18
  */
19
19
  export function isUsageLimitErrorMessage(errorMessage: string): boolean {
20
- return /usage.?limit|usage_limit_reached|limit_reached/i.test(errorMessage);
20
+ return /usage.?limit|usage_limit_reached|limit_reached|quota.?exhaust/i.test(errorMessage);
21
21
  }
22
22
 
23
23
  /**