fourmis-agents-sdk 0.2.6 → 0.3.0

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.
package/dist/index.js CHANGED
@@ -501,7 +501,8 @@ async function* agentLoop(prompt, options) {
501
501
  hooks,
502
502
  mcpClient,
503
503
  previousMessages,
504
- sessionLogger
504
+ sessionLogger,
505
+ nativeMemoryTool
505
506
  } = options;
506
507
  const startTime = Date.now();
507
508
  let apiTimeMs = 0;
@@ -555,13 +556,15 @@ async function* agentLoop(prompt, options) {
555
556
  let assistantTextParts = [];
556
557
  let toolCalls = [];
557
558
  let turnUsage = emptyTokenUsage();
559
+ const nativeTools = nativeMemoryTool ? [nativeMemoryTool.definition] : undefined;
558
560
  try {
559
561
  const chunks = provider.chat({
560
562
  model,
561
563
  messages,
562
564
  tools: toolDefs.length > 0 ? toolDefs : undefined,
563
565
  systemPrompt,
564
- signal
566
+ signal,
567
+ nativeTools
565
568
  });
566
569
  for await (const chunk of chunks) {
567
570
  switch (chunk.type) {
@@ -719,13 +722,24 @@ async function* agentLoop(prompt, options) {
719
722
  input: toolInput,
720
723
  uuid: uuid()
721
724
  };
722
- const toolCtx = {
723
- cwd,
724
- signal,
725
- sessionId,
726
- env
727
- };
728
- const result = await tools.execute(call.name, toolInput, toolCtx);
725
+ let result;
726
+ if (call.name === "memory" && nativeMemoryTool) {
727
+ try {
728
+ const content = await nativeMemoryTool.execute(toolInput);
729
+ result = { content, isError: content.startsWith("Error:") };
730
+ } catch (err) {
731
+ const message = err instanceof Error ? err.message : String(err);
732
+ result = { content: `Error: ${message}`, isError: true };
733
+ }
734
+ } else {
735
+ const toolCtx = {
736
+ cwd,
737
+ signal,
738
+ sessionId,
739
+ env
740
+ };
741
+ result = await tools.execute(call.name, toolInput, toolCtx);
742
+ }
729
743
  if (debug) {
730
744
  console.error(`[debug] Tool ${call.name}: ${result.isError ? "ERROR" : "OK"} (${result.content.length} chars)`);
731
745
  }
@@ -1040,15 +1054,29 @@ class AnthropicAdapter {
1040
1054
  } else if (request.systemPrompt) {
1041
1055
  params.system = request.systemPrompt;
1042
1056
  }
1057
+ const allTools = [];
1043
1058
  if (tools && tools.length > 0) {
1044
- params.tools = tools;
1059
+ allTools.push(...tools);
1060
+ }
1061
+ if (request.nativeTools && request.nativeTools.length > 0) {
1062
+ allTools.push(...request.nativeTools);
1063
+ }
1064
+ if (allTools.length > 0) {
1065
+ params.tools = allTools;
1045
1066
  }
1046
1067
  if (request.temperature !== undefined) {
1047
1068
  params.temperature = request.temperature;
1048
1069
  }
1049
- const stream = this.client.messages.stream(params, {
1070
+ const hasMemoryTool = request.nativeTools?.some((t) => t.type === "memory_20250818");
1071
+ const requestOptions = {
1050
1072
  signal: request.signal
1051
- });
1073
+ };
1074
+ if (hasMemoryTool) {
1075
+ requestOptions.headers = {
1076
+ "anthropic-beta": "context-management-2025-06-27"
1077
+ };
1078
+ }
1079
+ const stream = this.client.messages.stream(params, requestOptions);
1052
1080
  const toolInputBuffers = new Map;
1053
1081
  for await (const event of stream) {
1054
1082
  switch (event.type) {
@@ -3586,6 +3614,307 @@ function loadSessionMessages(cwd, sessionId) {
3586
3614
  return messages;
3587
3615
  }
3588
3616
 
3617
+ // src/memory/memory-handler.ts
3618
+ import { readdir, stat, readFile, writeFile, rm, rename, mkdir as mkdir2 } from "fs/promises";
3619
+ import { join as join6, resolve, relative } from "path";
3620
+ import { existsSync as existsSync4 } from "fs";
3621
+ function createMemoryHandler(memoryDir) {
3622
+ const absMemoryDir = resolve(memoryDir);
3623
+ function resolvePath2(logicalPath) {
3624
+ let cleaned = logicalPath;
3625
+ if (cleaned.startsWith("/memories")) {
3626
+ cleaned = cleaned.slice("/memories".length);
3627
+ }
3628
+ if (cleaned.startsWith("/")) {
3629
+ cleaned = cleaned.slice(1);
3630
+ }
3631
+ if (cleaned.includes("..") || cleaned.includes("%2e") || cleaned.includes("%2E")) {
3632
+ throw new Error(`Path traversal detected: ${logicalPath}`);
3633
+ }
3634
+ const absPath = cleaned === "" ? absMemoryDir : resolve(absMemoryDir, cleaned);
3635
+ const rel = relative(absMemoryDir, absPath);
3636
+ if (rel.startsWith("..") || resolve(absPath) !== absPath && !absPath.startsWith(absMemoryDir)) {
3637
+ throw new Error(`Path traversal detected: ${logicalPath}`);
3638
+ }
3639
+ return absPath;
3640
+ }
3641
+ function toLogicalPath(absPath) {
3642
+ const rel = relative(absMemoryDir, absPath);
3643
+ return rel === "" ? "/memories" : `/memories/${rel}`;
3644
+ }
3645
+ function formatSize(bytes) {
3646
+ if (bytes < 1024)
3647
+ return `${bytes}`;
3648
+ if (bytes < 1024 * 1024)
3649
+ return `${(bytes / 1024).toFixed(1)}K`;
3650
+ return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
3651
+ }
3652
+ async function listDir(dirPath, depth = 0) {
3653
+ const lines = [];
3654
+ const dirStat = await stat(dirPath);
3655
+ if (depth === 0) {
3656
+ lines.push(`${formatSize(dirStat.size)} ${toLogicalPath(dirPath)}`);
3657
+ }
3658
+ if (depth >= 2)
3659
+ return lines;
3660
+ try {
3661
+ const entries = await readdir(dirPath, { withFileTypes: true });
3662
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3663
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
3664
+ continue;
3665
+ const entryPath = join6(dirPath, entry.name);
3666
+ const entryStat = await stat(entryPath);
3667
+ lines.push(`${formatSize(entryStat.size)} ${toLogicalPath(entryPath)}`);
3668
+ if (entry.isDirectory()) {
3669
+ const subLines = await listDir(entryPath, depth + 1);
3670
+ lines.push(...subLines.slice(depth === 0 ? 0 : 0));
3671
+ }
3672
+ }
3673
+ } catch {}
3674
+ return lines;
3675
+ }
3676
+ function formatFileContent(content, viewRange) {
3677
+ const lines = content.split(`
3678
+ `);
3679
+ if (lines.length > 999999) {
3680
+ throw new Error(`File exceeds maximum line limit of 999,999 lines.`);
3681
+ }
3682
+ let start = 0;
3683
+ let end = lines.length;
3684
+ if (viewRange && viewRange.length >= 2) {
3685
+ start = Math.max(0, viewRange[0] - 1);
3686
+ end = Math.min(lines.length, viewRange[1]);
3687
+ }
3688
+ const formatted = [];
3689
+ for (let i = start;i < end; i++) {
3690
+ const lineNum = String(i + 1).padStart(6, " ");
3691
+ formatted.push(`${lineNum} ${lines[i]}`);
3692
+ }
3693
+ return formatted.join(`
3694
+ `);
3695
+ }
3696
+ async function handleView(cmd) {
3697
+ const absPath = resolvePath2(cmd.path);
3698
+ if (!existsSync4(absPath)) {
3699
+ return `The path ${cmd.path} does not exist. Please provide a valid path.`;
3700
+ }
3701
+ const s = await stat(absPath);
3702
+ if (s.isDirectory()) {
3703
+ const lines = await listDir(absPath);
3704
+ return `Here're the files and directories up to 2 levels deep in ${cmd.path}, excluding hidden items and node_modules:
3705
+ ${lines.join(`
3706
+ `)}`;
3707
+ }
3708
+ const content = await readFile(absPath, "utf-8");
3709
+ const formatted = formatFileContent(content, cmd.view_range);
3710
+ return `Here's the content of ${cmd.path} with line numbers:
3711
+ ${formatted}`;
3712
+ }
3713
+ async function handleCreate(cmd) {
3714
+ const absPath = resolvePath2(cmd.path);
3715
+ if (existsSync4(absPath)) {
3716
+ return `Error: File ${cmd.path} already exists`;
3717
+ }
3718
+ const parentDir = resolve(absPath, "..");
3719
+ await mkdir2(parentDir, { recursive: true });
3720
+ await writeFile(absPath, cmd.file_text, "utf-8");
3721
+ return `File created successfully at: ${cmd.path}`;
3722
+ }
3723
+ async function handleStrReplace(cmd) {
3724
+ const absPath = resolvePath2(cmd.path);
3725
+ if (!existsSync4(absPath)) {
3726
+ return `Error: The path ${cmd.path} does not exist. Please provide a valid path.`;
3727
+ }
3728
+ const s = await stat(absPath);
3729
+ if (s.isDirectory()) {
3730
+ return `Error: The path ${cmd.path} does not exist. Please provide a valid path.`;
3731
+ }
3732
+ const content = await readFile(absPath, "utf-8");
3733
+ const lines = content.split(`
3734
+ `);
3735
+ const matchingLines = [];
3736
+ let searchPos = 0;
3737
+ let occurrences = 0;
3738
+ while (true) {
3739
+ const idx = content.indexOf(cmd.old_str, searchPos);
3740
+ if (idx === -1)
3741
+ break;
3742
+ occurrences++;
3743
+ const lineNum = content.substring(0, idx).split(`
3744
+ `).length;
3745
+ matchingLines.push(lineNum);
3746
+ searchPos = idx + cmd.old_str.length;
3747
+ }
3748
+ if (occurrences === 0) {
3749
+ return `No replacement was performed, old_str \`${cmd.old_str}\` did not appear verbatim in ${cmd.path}.`;
3750
+ }
3751
+ if (occurrences > 1) {
3752
+ return `No replacement was performed. Multiple occurrences of old_str \`${cmd.old_str}\` in lines: ${matchingLines.join(", ")}. Please ensure it is unique`;
3753
+ }
3754
+ const newContent = content.replace(cmd.old_str, cmd.new_str);
3755
+ await writeFile(absPath, newContent, "utf-8");
3756
+ const newLines = newContent.split(`
3757
+ `);
3758
+ const replaceLine = matchingLines[0];
3759
+ const snippetStart = Math.max(0, replaceLine - 3);
3760
+ const snippetEnd = Math.min(newLines.length, replaceLine + 3);
3761
+ const snippet = newLines.slice(snippetStart, snippetEnd).map((line, i) => `${String(snippetStart + i + 1).padStart(6, " ")} ${line}`).join(`
3762
+ `);
3763
+ return `The memory file has been edited.
3764
+ ${snippet}`;
3765
+ }
3766
+ async function handleInsert(cmd) {
3767
+ const absPath = resolvePath2(cmd.path);
3768
+ if (!existsSync4(absPath)) {
3769
+ return `Error: The path ${cmd.path} does not exist`;
3770
+ }
3771
+ const s = await stat(absPath);
3772
+ if (s.isDirectory()) {
3773
+ return `Error: The path ${cmd.path} does not exist`;
3774
+ }
3775
+ const content = await readFile(absPath, "utf-8");
3776
+ const lines = content.split(`
3777
+ `);
3778
+ if (cmd.insert_line < 0 || cmd.insert_line > lines.length) {
3779
+ return `Error: Invalid \`insert_line\` parameter: ${cmd.insert_line}. It should be within the range of lines of the file: [0, ${lines.length}]`;
3780
+ }
3781
+ const insertLines = cmd.insert_text.split(`
3782
+ `);
3783
+ lines.splice(cmd.insert_line, 0, ...insertLines);
3784
+ await writeFile(absPath, lines.join(`
3785
+ `), "utf-8");
3786
+ return `The file ${cmd.path} has been edited.`;
3787
+ }
3788
+ async function handleDelete(cmd) {
3789
+ const absPath = resolvePath2(cmd.path);
3790
+ if (!existsSync4(absPath)) {
3791
+ return `Error: The path ${cmd.path} does not exist`;
3792
+ }
3793
+ await rm(absPath, { recursive: true, force: true });
3794
+ return `Successfully deleted ${cmd.path}`;
3795
+ }
3796
+ async function handleRename(cmd) {
3797
+ const oldAbs = resolvePath2(cmd.old_path);
3798
+ const newAbs = resolvePath2(cmd.new_path);
3799
+ if (!existsSync4(oldAbs)) {
3800
+ return `Error: The path ${cmd.old_path} does not exist`;
3801
+ }
3802
+ if (existsSync4(newAbs)) {
3803
+ return `Error: The destination ${cmd.new_path} already exists`;
3804
+ }
3805
+ const parentDir = resolve(newAbs, "..");
3806
+ await mkdir2(parentDir, { recursive: true });
3807
+ await rename(oldAbs, newAbs);
3808
+ return `Successfully renamed ${cmd.old_path} to ${cmd.new_path}`;
3809
+ }
3810
+ async function execute(cmd) {
3811
+ if (!existsSync4(absMemoryDir)) {
3812
+ await mkdir2(absMemoryDir, { recursive: true });
3813
+ }
3814
+ switch (cmd.command) {
3815
+ case "view":
3816
+ return handleView(cmd);
3817
+ case "create":
3818
+ return handleCreate(cmd);
3819
+ case "str_replace":
3820
+ return handleStrReplace(cmd);
3821
+ case "insert":
3822
+ return handleInsert(cmd);
3823
+ case "delete":
3824
+ return handleDelete(cmd);
3825
+ case "rename":
3826
+ return handleRename(cmd);
3827
+ default:
3828
+ return `Error: Unknown command: ${cmd.command}`;
3829
+ }
3830
+ }
3831
+ return { execute, resolvePath: resolvePath2, toLogicalPath };
3832
+ }
3833
+ // src/memory/index.ts
3834
+ function createNativeMemoryTool(config) {
3835
+ const handler = createMemoryHandler(config.path);
3836
+ return {
3837
+ definition: { type: "memory_20250818", name: "memory" },
3838
+ execute: (cmd) => handler.execute(cmd)
3839
+ };
3840
+ }
3841
+ function createMemoryTool(config) {
3842
+ const handler = createMemoryHandler(config.path);
3843
+ return {
3844
+ name: "memory",
3845
+ description: `Manage persistent memory files. Supports 6 commands:
3846
+ ` + `- view: Show directory listing or file contents (path, optional view_range)
3847
+ ` + `- create: Create a new file (path, file_text)
3848
+ ` + `- str_replace: Replace text in a file (path, old_str, new_str)
3849
+ ` + `- insert: Insert text at a line number (path, insert_line, insert_text)
3850
+ ` + `- delete: Delete a file or directory (path)
3851
+ ` + `- rename: Rename/move a file or directory (old_path, new_path)
3852
+
3853
+ ` + `All paths should start with /memories/. Example: /memories/notes.txt
3854
+
3855
+ ` + "IMPORTANT: Always view your memory directory before starting any task.",
3856
+ inputSchema: {
3857
+ type: "object",
3858
+ properties: {
3859
+ command: {
3860
+ type: "string",
3861
+ enum: ["view", "create", "str_replace", "insert", "delete", "rename"],
3862
+ description: "The memory operation to perform"
3863
+ },
3864
+ path: {
3865
+ type: "string",
3866
+ description: "Path to the file or directory (starts with /memories/)"
3867
+ },
3868
+ file_text: {
3869
+ type: "string",
3870
+ description: "Content for the 'create' command"
3871
+ },
3872
+ old_str: {
3873
+ type: "string",
3874
+ description: "Text to find for 'str_replace' command"
3875
+ },
3876
+ new_str: {
3877
+ type: "string",
3878
+ description: "Replacement text for 'str_replace' command"
3879
+ },
3880
+ insert_line: {
3881
+ type: "number",
3882
+ description: "Line number for 'insert' command"
3883
+ },
3884
+ insert_text: {
3885
+ type: "string",
3886
+ description: "Text to insert for 'insert' command"
3887
+ },
3888
+ old_path: {
3889
+ type: "string",
3890
+ description: "Source path for 'rename' command"
3891
+ },
3892
+ new_path: {
3893
+ type: "string",
3894
+ description: "Destination path for 'rename' command"
3895
+ },
3896
+ view_range: {
3897
+ type: "array",
3898
+ items: { type: "number" },
3899
+ description: "Optional [start, end] line range for 'view' command"
3900
+ }
3901
+ },
3902
+ required: ["command"]
3903
+ },
3904
+ async execute(input) {
3905
+ try {
3906
+ const cmd = input;
3907
+ const result = await handler.execute(cmd);
3908
+ const isError = result.startsWith("Error:");
3909
+ return { content: result, isError };
3910
+ } catch (err) {
3911
+ const message = err instanceof Error ? err.message : String(err);
3912
+ return { content: `Error: ${message}`, isError: true };
3913
+ }
3914
+ }
3915
+ };
3916
+ }
3917
+
3589
3918
  // src/api.ts
3590
3919
  var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
3591
3920
  var DEFAULT_MAX_TURNS = 10;
@@ -3682,6 +4011,15 @@ function query(params) {
3682
4011
  registry.register(createTaskOutputTool(taskManager));
3683
4012
  registry.register(createTaskStopTool(taskManager));
3684
4013
  }
4014
+ let nativeMemoryTool;
4015
+ if (options.memoryPath) {
4016
+ const memoryConfig = { path: options.memoryPath };
4017
+ if (providerName === "anthropic") {
4018
+ nativeMemoryTool = createNativeMemoryTool(memoryConfig);
4019
+ } else {
4020
+ registry.register(createMemoryTool(memoryConfig));
4021
+ }
4022
+ }
3685
4023
  const generator = agentLoop(prompt, {
3686
4024
  provider,
3687
4025
  model,
@@ -3699,7 +4037,8 @@ function query(params) {
3699
4037
  hooks: hookManager,
3700
4038
  mcpClient,
3701
4039
  previousMessages,
3702
- sessionLogger
4040
+ sessionLogger,
4041
+ nativeMemoryTool
3703
4042
  });
3704
4043
  return createQuery(generator, abortController);
3705
4044
  }
@@ -3733,6 +4072,9 @@ export {
3733
4072
  query,
3734
4073
  tool as mcpTool,
3735
4074
  getProvider,
4075
+ createNativeMemoryTool,
4076
+ createMemoryTool,
4077
+ createMemoryHandler,
3736
4078
  createMcpServer,
3737
4079
  WriteTool,
3738
4080
  ToolRegistry,
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Memory module — provider-agnostic memory tool.
3
+ *
4
+ * For Anthropic: Returns a native memory tool config that the provider
5
+ * handles specially (type: "memory_20250818" + beta header).
6
+ *
7
+ * For OpenAI/Gemini: Returns a regular ToolImplementation that the
8
+ * agent can call just like any other tool.
9
+ */
10
+ export { createMemoryHandler } from "./memory-handler.js";
11
+ export type { MemoryCommand } from "./memory-handler.js";
12
+ import type { MemoryCommand } from "./memory-handler.js";
13
+ import type { ToolImplementation } from "../tools/registry.js";
14
+ export type MemoryConfig = {
15
+ /** Absolute path to the memory directory (e.g. /workspace/memories/) */
16
+ path: string;
17
+ };
18
+ /**
19
+ * For Anthropic provider: returns the native memory tool definition
20
+ * to include in the API request, plus handler functions for each command.
21
+ *
22
+ * The Anthropic provider needs to:
23
+ * 1. Add `{type: "memory_20250818", name: "memory"}` to the tools array
24
+ * 2. Add `context-management-2025-06-27` beta header
25
+ * 3. Route "memory" tool calls to the handler
26
+ */
27
+ export type NativeMemoryTool = {
28
+ /** The tool definition for the Anthropic API (type: "memory_20250818") */
29
+ definition: {
30
+ type: "memory_20250818";
31
+ name: "memory";
32
+ };
33
+ /** Execute a memory command */
34
+ execute: (cmd: MemoryCommand) => Promise<string>;
35
+ };
36
+ export declare function createNativeMemoryTool(config: MemoryConfig): NativeMemoryTool;
37
+ /**
38
+ * For non-Anthropic providers: wraps memory as a standard ToolImplementation
39
+ * so any LLM can call it as a regular function tool.
40
+ */
41
+ export declare function createMemoryTool(config: MemoryConfig): ToolImplementation;
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/memory/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAI/D,MAAM,MAAM,YAAY,GAAG;IACzB,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAIF;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,0EAA0E;IAC1E,UAAU,EAAE;QAAE,IAAI,EAAE,iBAAiB,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC;IACxD,+BAA+B;IAC/B,OAAO,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAClD,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,GAAG,gBAAgB,CAM7E;AAID;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB,CA4EzE"}