@aigne/core 1.72.0-beta.4 → 1.72.0-beta.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.72.0-beta.6](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.72.0-beta.5...core-v1.72.0-beta.6) (2025-12-25)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **core:** passthrough model options in chat model ([#856](https://github.com/AIGNE-io/aigne-framework/issues/856)) ([41387bd](https://github.com/AIGNE-io/aigne-framework/commit/41387bde0a615080ea5d665e998afb0b9c32c5fd))
9
+
10
+ ## [1.72.0-beta.5](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.72.0-beta.4...core-v1.72.0-beta.5) (2025-12-25)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **core:** afs skills improvements ([#849](https://github.com/AIGNE-io/aigne-framework/issues/849)) ([557cc8b](https://github.com/AIGNE-io/aigne-framework/commit/557cc8b4b72f0e91ad654556f47bbe0ad0ececdb))
16
+
3
17
  ## [1.72.0-beta.4](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.72.0-beta.3...core-v1.72.0-beta.4) (2025-12-24)
4
18
 
5
19
 
@@ -378,15 +378,29 @@ const modelOptionsSchemaProperties = {
378
378
  zod_1.z.literal("medium"),
379
379
  zod_1.z.literal("high"),
380
380
  ]),
381
+ cacheConfig: zod_1.z.object({
382
+ enabled: (0, schema_js_1.optionalize)(zod_1.z.boolean().default(true)),
383
+ ttl: (0, schema_js_1.optionalize)(zod_1.z.union([zod_1.z.literal("5m"), zod_1.z.literal("1h"), zod_1.z.number()]).default("5m")),
384
+ strategy: (0, schema_js_1.optionalize)(zod_1.z.union([zod_1.z.literal("auto"), zod_1.z.literal("manual")]).default("auto")),
385
+ autoBreakpoints: (0, schema_js_1.optionalize)(zod_1.z.object({
386
+ tools: (0, schema_js_1.optionalize)(zod_1.z.boolean().default(true)),
387
+ system: (0, schema_js_1.optionalize)(zod_1.z.boolean().default(true)),
388
+ lastMessage: (0, schema_js_1.optionalize)(zod_1.z.boolean().default(false)),
389
+ })),
390
+ }),
381
391
  };
382
- const modelOptionsSchema = zod_1.z.object(Object.fromEntries(Object.entries(modelOptionsSchemaProperties).map(([key, schema]) => [
392
+ const modelOptionsSchema = zod_1.z
393
+ .object(Object.fromEntries(Object.entries(modelOptionsSchemaProperties).map(([key, schema]) => [
383
394
  key,
384
395
  (0, schema_js_1.optionalize)(schema),
385
- ])));
386
- const modelOptionsWithGetterSchema = zod_1.z.object(Object.fromEntries(Object.entries(modelOptionsSchemaProperties).map(([key, schema]) => [
396
+ ])))
397
+ .passthrough();
398
+ const modelOptionsWithGetterSchema = zod_1.z
399
+ .object(Object.fromEntries(Object.entries(modelOptionsSchemaProperties).map(([key, schema]) => [
387
400
  key,
388
401
  (0, schema_js_1.optionalize)((0, agent_js_1.getterSchema)(schema)),
389
- ])));
402
+ ])))
403
+ .passthrough();
390
404
  const chatModelOptionsSchema = agent_js_1.agentOptionsSchema.extend({
391
405
  model: (0, schema_js_1.optionalize)(zod_1.z.string()),
392
406
  modelOptions: (0, schema_js_1.optionalize)(modelOptionsWithGetterSchema),
@@ -1,21 +1,17 @@
1
1
  import type { AgentInvokeOptions, AgentOptions, Message } from "../../../agents/agent.js";
2
2
  import { AFSSkillBase } from "./base.js";
3
- export interface Patch {
4
- start_line: number;
5
- end_line: number;
6
- replace?: string;
7
- delete: boolean;
8
- }
9
3
  export interface AFSEditInput extends Message {
10
4
  path: string;
11
- patches: Patch[];
5
+ oldString: string;
6
+ newString: string;
7
+ replaceAll?: boolean;
12
8
  }
13
9
  export interface AFSEditOutput extends Message {
14
10
  status: string;
15
11
  tool: string;
16
12
  path: string;
17
13
  message: string;
18
- data: string;
14
+ snippet: string;
19
15
  }
20
16
  export interface AFSEditAgentOptions extends AgentOptions<AFSEditInput, AFSEditOutput> {
21
17
  afs: NonNullable<AgentOptions<AFSEditInput, AFSEditOutput>["afs"]>;
@@ -23,5 +19,6 @@ export interface AFSEditAgentOptions extends AgentOptions<AFSEditInput, AFSEditO
23
19
  export declare class AFSEditAgent extends AFSSkillBase<AFSEditInput, AFSEditOutput> {
24
20
  constructor(options: AFSEditAgentOptions);
25
21
  process(input: AFSEditInput, _options: AgentInvokeOptions): Promise<AFSEditOutput>;
26
- applyCustomPatches(text: string, patches: Patch[]): string;
22
+ private countOccurrences;
23
+ private extractSnippet;
27
24
  }
@@ -3,119 +3,122 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AFSEditAgent = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const base_js_1 = require("./base.js");
6
+ const CONTEXT_LINES = 4; // Number of lines to show before and after the edit
6
7
  class AFSEditAgent extends base_js_1.AFSSkillBase {
7
8
  constructor(options) {
8
9
  super({
9
10
  name: "afs_edit",
10
- description: `Apply precise line-based patches to modify files in the Agentic File System (AFS)
11
- - Performs targeted edits using line numbers without rewriting the entire file
12
- - Supports both replacing and deleting line ranges
13
- - Multiple patches can be applied in a single operation
11
+ description: `Performs exact string replacements in files within the Agentic File System (AFS).
14
12
 
15
13
  Usage:
14
+ - You must use afs_read at least once before editing to understand the file content
16
15
  - The path must be an absolute AFS path starting with "/" (e.g., "/docs/readme.md")
17
- - This is NOT a local system file path - it operates within the AFS virtual file system
18
- - IMPORTANT: You MUST use afs_read with withLineNumbers=true before editing to get accurate line numbers
19
- - Line numbers are 0-based: first line is 0, second line is 1, etc.
20
- - The range [start_line, end_line) is exclusive on end_line`,
16
+ - Preserve exact indentation (tabs/spaces) as it appears in the file
17
+ - The edit will FAIL if oldString is not found in the file
18
+ - The edit will FAIL if oldString appears multiple times (unless replaceAll is true)
19
+ - Use replaceAll to replace/rename strings across the entire file`,
21
20
  ...options,
22
21
  inputSchema: zod_1.z.object({
23
22
  path: zod_1.z
24
23
  .string()
25
24
  .describe("Absolute AFS path to the file to edit (e.g., '/docs/readme.md'). Must start with '/'"),
26
- patches: zod_1.z
27
- .array(zod_1.z.object({
28
- start_line: zod_1.z
29
- .number()
30
- .int()
31
- .describe("Start line number (0-based, inclusive). First line is 0"),
32
- end_line: zod_1.z
33
- .number()
34
- .int()
35
- .describe("End line number (0-based, exclusive). To edit line 5 only, use start_line=5, end_line=6"),
36
- replace: zod_1.z
37
- .string()
38
- .optional()
39
- .describe("New content to insert. Omit when delete=true"),
40
- delete: zod_1.z
41
- .boolean()
42
- .describe("Set to true to delete the line range. Set to false to replace with 'replace' content"),
43
- }))
44
- .min(1)
45
- .describe("Array of patches to apply. Each patch specifies a line range and the operation (delete or replace)"),
25
+ oldString: zod_1.z
26
+ .string()
27
+ .describe("The exact text to replace. Must match file content exactly including whitespace"),
28
+ newString: zod_1.z
29
+ .string()
30
+ .describe("The text to replace it with (must be different from oldString)"),
31
+ replaceAll: zod_1.z
32
+ .boolean()
33
+ .optional()
34
+ .default(false)
35
+ .describe("Replace all occurrences of oldString (default: false)"),
46
36
  }),
47
37
  outputSchema: zod_1.z.object({
48
38
  status: zod_1.z.string(),
49
39
  tool: zod_1.z.string(),
50
40
  path: zod_1.z.string(),
51
41
  message: zod_1.z.string(),
52
- data: zod_1.z.string(),
42
+ snippet: zod_1.z.string(),
53
43
  }),
54
44
  });
55
45
  }
56
46
  async process(input, _options) {
57
47
  if (!this.afs)
58
48
  throw new Error("AFS is not configured for this agent.");
59
- if (!input.patches?.length) {
60
- throw new Error("No patches provided for afs_edit.");
49
+ const { path, oldString, newString, replaceAll = false } = input;
50
+ if (oldString === newString) {
51
+ throw new Error("oldString and newString must be different");
61
52
  }
62
- const readResult = await this.afs.read(input.path);
53
+ const readResult = await this.afs.read(path);
63
54
  if (!readResult.data?.content || typeof readResult.data.content !== "string") {
64
- throw new Error(`Cannot read file content from: ${input.path}`);
55
+ throw new Error(`Cannot read file content from: ${path}`);
65
56
  }
66
57
  const originalContent = readResult.data.content;
67
- const updatedContent = this.applyCustomPatches(originalContent, input.patches);
68
- await this.afs.write(input.path, {
58
+ // Check if oldString exists in the file
59
+ const occurrences = this.countOccurrences(originalContent, oldString);
60
+ if (occurrences === 0) {
61
+ throw new Error(`oldString not found in file: ${path}`);
62
+ }
63
+ if (occurrences > 1 && !replaceAll) {
64
+ throw new Error(`oldString appears ${occurrences} times in file. Use replaceAll=true to replace all occurrences, or provide more context to make oldString unique.`);
65
+ }
66
+ // Find the position of the first occurrence for snippet extraction
67
+ const firstOccurrenceIndex = originalContent.indexOf(oldString);
68
+ // Perform the replacement
69
+ const updatedContent = replaceAll
70
+ ? originalContent.split(oldString).join(newString)
71
+ : originalContent.replace(oldString, newString);
72
+ await this.afs.write(path, {
69
73
  content: updatedContent,
70
74
  });
75
+ // Generate snippet around the edit location
76
+ const snippet = this.extractSnippet(updatedContent, firstOccurrenceIndex, newString.length);
77
+ const replacementCount = replaceAll ? occurrences : 1;
71
78
  return {
72
79
  status: "success",
73
80
  tool: "afs_edit",
74
- path: input.path,
75
- message: `Applied ${input.patches.length} patches to ${input.path}`,
76
- data: updatedContent,
81
+ path,
82
+ message: `Replaced ${replacementCount} occurrence${replacementCount > 1 ? "s" : ""} in ${path}`,
83
+ snippet,
77
84
  };
78
85
  }
79
- applyCustomPatches(text, patches) {
80
- // Sort by start_line to ensure sequential application
81
- const sorted = [...patches].sort((a, b) => a.start_line - b.start_line);
82
- const lines = text.split("\n");
83
- for (let i = 0; i < sorted.length; i++) {
84
- const patch = sorted[i];
85
- if (!patch)
86
- continue;
87
- const start = patch.start_line;
88
- const end = patch.end_line;
89
- const deleteCount = end - start; // [start, end) range
90
- let delta = 0;
91
- if (patch.delete) {
92
- // Delete mode: remove the specified lines [start, end)
93
- lines.splice(start, deleteCount);
94
- delta = -deleteCount;
95
- }
96
- else {
97
- // Replace mode: replace the specified lines with new content
98
- const replaceLines = patch.replace ? patch.replace.split("\n") : [];
99
- lines.splice(start, deleteCount, ...replaceLines);
100
- delta = replaceLines.length - deleteCount;
101
- }
102
- // Update subsequent patches' line numbers
103
- // For exclusive-end semantics [start, end), we adjust patches that start >= current patch's start_line
104
- // after the current patch has been applied
105
- if (delta !== 0) {
106
- for (let j = i + 1; j < sorted.length; j++) {
107
- const next = sorted[j];
108
- if (!next)
109
- continue;
110
- // Adjust patches that start at or after the current patch's end line
111
- if (next.start_line >= patch.end_line) {
112
- next.start_line += delta;
113
- next.end_line += delta;
114
- }
115
- }
86
+ countOccurrences(text, search) {
87
+ let count = 0;
88
+ let position = text.indexOf(search);
89
+ while (position !== -1) {
90
+ count++;
91
+ position = text.indexOf(search, position + search.length);
92
+ }
93
+ return count;
94
+ }
95
+ extractSnippet(content, editStartIndex, newStringLength) {
96
+ const lines = content.split("\n");
97
+ // Find the line number where the edit starts
98
+ let charCount = 0;
99
+ let editStartLine = 0;
100
+ for (let i = 0; i < lines.length; i++) {
101
+ const lineLength = (lines[i]?.length ?? 0) + 1; // +1 for newline
102
+ if (charCount + lineLength > editStartIndex) {
103
+ editStartLine = i;
104
+ break;
116
105
  }
106
+ charCount += lineLength;
117
107
  }
118
- return lines.join("\n");
108
+ // Calculate how many lines the new content spans
109
+ const newContentLines = content
110
+ .substring(editStartIndex, editStartIndex + newStringLength)
111
+ .split("\n").length;
112
+ const editEndLine = editStartLine + newContentLines - 1;
113
+ // Extract lines with context
114
+ const startLine = Math.max(0, editStartLine - CONTEXT_LINES);
115
+ const endLine = Math.min(lines.length - 1, editEndLine + CONTEXT_LINES);
116
+ // Format with line numbers (1-based)
117
+ const snippetLines = lines.slice(startLine, endLine + 1).map((line, idx) => {
118
+ const lineNum = startLine + idx + 1;
119
+ return `${String(lineNum).padStart(4)}| ${line}`;
120
+ });
121
+ return snippetLines.join("\n");
119
122
  }
120
123
  }
121
124
  exports.AFSEditAgent = AFSEditAgent;
@@ -3,15 +3,19 @@ import type { AgentInvokeOptions, AgentOptions, Message } from "../../../agents/
3
3
  import { AFSSkillBase } from "./base.js";
4
4
  export interface AFSReadInput extends Message {
5
5
  path: string;
6
- withLineNumbers?: boolean;
6
+ offset?: number;
7
+ limit?: number;
7
8
  }
8
9
  export interface AFSReadOutput extends Message {
9
10
  status: string;
10
11
  tool: string;
11
12
  path: string;
12
- withLineNumbers?: boolean;
13
13
  data?: AFSEntry;
14
14
  message?: string;
15
+ totalLines?: number;
16
+ returnedLines?: number;
17
+ truncated?: boolean;
18
+ offset?: number;
15
19
  }
16
20
  export interface AFSReadAgentOptions extends AgentOptions<AFSReadInput, AFSReadOutput> {
17
21
  afs: NonNullable<AgentOptions<AFSReadInput, AFSReadOutput>["afs"]>;
@@ -3,37 +3,51 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AFSReadAgent = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const base_js_1 = require("./base.js");
6
+ const DEFAULT_LINE_LIMIT = 2000;
7
+ const MAX_LINE_LENGTH = 2000;
6
8
  class AFSReadAgent extends base_js_1.AFSSkillBase {
7
9
  constructor(options) {
8
10
  super({
9
11
  name: "afs_read",
10
12
  description: `Read file contents from the Agentic File System (AFS)
11
- - Returns the complete content of a file at the specified AFS path
12
- - Supports line numbers output for precise editing references
13
- - Use this tool when you need to review, analyze, or understand file content
13
+ - Returns the content of a file at the specified AFS path
14
+ - By default reads up to ${DEFAULT_LINE_LIMIT} lines, use offset/limit for large files
15
+ - Lines longer than ${MAX_LINE_LENGTH} characters will be truncated
14
16
 
15
17
  Usage:
16
- - The path must be an absolute AFS path starting with "/" (e.g., "/docs/readme.md", "/memory/user/notes")
17
- - This is NOT a local system file path - it operates within the AFS virtual file system
18
- - IMPORTANT: You MUST set withLineNumbers to true before using afs_edit, as line numbers are required for precise edits
19
- - Returns the file's content along with metadata (id, path, timestamps, etc.)`,
18
+ - The path must be an absolute AFS path starting with "/" (e.g., "/docs/readme.md")
19
+ - Use offset to start reading from a specific line (0-based)
20
+ - Use limit to control number of lines returned (default: ${DEFAULT_LINE_LIMIT})
21
+ - Check truncated field to know if file was partially returned`,
20
22
  ...options,
21
23
  inputSchema: zod_1.z.object({
22
24
  path: zod_1.z
23
25
  .string()
24
26
  .describe("Absolute AFS path to the file to read (e.g., '/docs/readme.md'). Must start with '/'"),
25
- withLineNumbers: zod_1.z
26
- .boolean()
27
+ offset: zod_1.z
28
+ .number()
29
+ .int()
30
+ .min(0)
27
31
  .optional()
28
- .describe("MUST be set to true before using afs_edit. Adds line numbers to output (format: '1| line content')"),
32
+ .describe("Line number to start reading from (0-based, default: 0)"),
33
+ limit: zod_1.z
34
+ .number()
35
+ .int()
36
+ .min(1)
37
+ .max(DEFAULT_LINE_LIMIT)
38
+ .optional()
39
+ .describe(`Maximum number of lines to read (default: ${DEFAULT_LINE_LIMIT})`),
29
40
  }),
30
41
  outputSchema: zod_1.z.object({
31
42
  status: zod_1.z.string(),
32
43
  tool: zod_1.z.string(),
33
44
  path: zod_1.z.string(),
34
- withLineNumbers: zod_1.z.boolean().optional(),
35
45
  data: zod_1.z.custom().optional(),
36
46
  message: zod_1.z.string().optional(),
47
+ totalLines: zod_1.z.number().optional(),
48
+ returnedLines: zod_1.z.number().optional(),
49
+ truncated: zod_1.z.boolean().optional(),
50
+ offset: zod_1.z.number().optional(),
37
51
  }),
38
52
  });
39
53
  }
@@ -41,22 +55,44 @@ Usage:
41
55
  if (!this.afs)
42
56
  throw new Error("AFS is not configured for this agent.");
43
57
  const result = await this.afs.read(input.path);
44
- let content = result.data?.content;
45
- if (input.withLineNumbers && typeof content === "string") {
46
- content = content
47
- .split("\n")
48
- .map((line, idx) => `${idx + 1}| ${line}`)
49
- .join("\n");
58
+ if (!result.data?.content || typeof result.data.content !== "string") {
59
+ return {
60
+ status: "success",
61
+ tool: "afs_read",
62
+ path: input.path,
63
+ ...result,
64
+ };
65
+ }
66
+ const offset = input.offset ?? 0;
67
+ const limit = input.limit ?? DEFAULT_LINE_LIMIT;
68
+ const allLines = result.data.content.split("\n");
69
+ const totalLines = allLines.length;
70
+ // Apply offset and limit
71
+ const selectedLines = allLines.slice(offset, offset + limit);
72
+ // Truncate long lines
73
+ const processedLines = selectedLines.map((line) => line.length > MAX_LINE_LENGTH ? `${line.substring(0, MAX_LINE_LENGTH)}... [truncated]` : line);
74
+ const returnedLines = processedLines.length;
75
+ const truncated = offset > 0 || offset + limit < totalLines;
76
+ const processedContent = processedLines.join("\n");
77
+ let message;
78
+ if (truncated) {
79
+ const startLine = offset + 1;
80
+ const endLine = offset + returnedLines;
81
+ message = `Showing lines ${startLine}-${endLine} of ${totalLines}. Use offset/limit to read more.`;
50
82
  }
51
83
  return {
52
84
  status: "success",
53
85
  tool: "afs_read",
54
86
  path: input.path,
55
- withLineNumbers: input.withLineNumbers,
87
+ totalLines,
88
+ returnedLines,
89
+ truncated,
90
+ offset,
91
+ message,
56
92
  ...result,
57
- data: result.data && {
93
+ data: {
58
94
  ...result.data,
59
- content,
95
+ content: processedContent,
60
96
  },
61
97
  };
62
98
  }
@@ -43,7 +43,7 @@ Usage:
43
43
  async process(input, _options) {
44
44
  if (!this.afs)
45
45
  throw new Error("AFS is not configured for this agent.");
46
- const result = await this.afs.write(input.path, {
46
+ const _result = await this.afs.write(input.path, {
47
47
  content: input.content,
48
48
  }, {
49
49
  append: input.append ?? false,
@@ -52,7 +52,7 @@ Usage:
52
52
  status: "success",
53
53
  tool: "afs_write",
54
54
  path: input.path,
55
- ...result,
55
+ message: "File written successfully",
56
56
  };
57
57
  }
58
58
  }
@@ -1,21 +1,17 @@
1
1
  import type { AgentInvokeOptions, AgentOptions, Message } from "../../../agents/agent.js";
2
2
  import { AFSSkillBase } from "./base.js";
3
- export interface Patch {
4
- start_line: number;
5
- end_line: number;
6
- replace?: string;
7
- delete: boolean;
8
- }
9
3
  export interface AFSEditInput extends Message {
10
4
  path: string;
11
- patches: Patch[];
5
+ oldString: string;
6
+ newString: string;
7
+ replaceAll?: boolean;
12
8
  }
13
9
  export interface AFSEditOutput extends Message {
14
10
  status: string;
15
11
  tool: string;
16
12
  path: string;
17
13
  message: string;
18
- data: string;
14
+ snippet: string;
19
15
  }
20
16
  export interface AFSEditAgentOptions extends AgentOptions<AFSEditInput, AFSEditOutput> {
21
17
  afs: NonNullable<AgentOptions<AFSEditInput, AFSEditOutput>["afs"]>;
@@ -23,5 +19,6 @@ export interface AFSEditAgentOptions extends AgentOptions<AFSEditInput, AFSEditO
23
19
  export declare class AFSEditAgent extends AFSSkillBase<AFSEditInput, AFSEditOutput> {
24
20
  constructor(options: AFSEditAgentOptions);
25
21
  process(input: AFSEditInput, _options: AgentInvokeOptions): Promise<AFSEditOutput>;
26
- applyCustomPatches(text: string, patches: Patch[]): string;
22
+ private countOccurrences;
23
+ private extractSnippet;
27
24
  }
@@ -3,15 +3,19 @@ import type { AgentInvokeOptions, AgentOptions, Message } from "../../../agents/
3
3
  import { AFSSkillBase } from "./base.js";
4
4
  export interface AFSReadInput extends Message {
5
5
  path: string;
6
- withLineNumbers?: boolean;
6
+ offset?: number;
7
+ limit?: number;
7
8
  }
8
9
  export interface AFSReadOutput extends Message {
9
10
  status: string;
10
11
  tool: string;
11
12
  path: string;
12
- withLineNumbers?: boolean;
13
13
  data?: AFSEntry;
14
14
  message?: string;
15
+ totalLines?: number;
16
+ returnedLines?: number;
17
+ truncated?: boolean;
18
+ offset?: number;
15
19
  }
16
20
  export interface AFSReadAgentOptions extends AgentOptions<AFSReadInput, AFSReadOutput> {
17
21
  afs: NonNullable<AgentOptions<AFSReadInput, AFSReadOutput>["afs"]>;
@@ -340,15 +340,29 @@ const modelOptionsSchemaProperties = {
340
340
  z.literal("medium"),
341
341
  z.literal("high"),
342
342
  ]),
343
+ cacheConfig: z.object({
344
+ enabled: optionalize(z.boolean().default(true)),
345
+ ttl: optionalize(z.union([z.literal("5m"), z.literal("1h"), z.number()]).default("5m")),
346
+ strategy: optionalize(z.union([z.literal("auto"), z.literal("manual")]).default("auto")),
347
+ autoBreakpoints: optionalize(z.object({
348
+ tools: optionalize(z.boolean().default(true)),
349
+ system: optionalize(z.boolean().default(true)),
350
+ lastMessage: optionalize(z.boolean().default(false)),
351
+ })),
352
+ }),
343
353
  };
344
- const modelOptionsSchema = z.object(Object.fromEntries(Object.entries(modelOptionsSchemaProperties).map(([key, schema]) => [
354
+ const modelOptionsSchema = z
355
+ .object(Object.fromEntries(Object.entries(modelOptionsSchemaProperties).map(([key, schema]) => [
345
356
  key,
346
357
  optionalize(schema),
347
- ])));
348
- const modelOptionsWithGetterSchema = z.object(Object.fromEntries(Object.entries(modelOptionsSchemaProperties).map(([key, schema]) => [
358
+ ])))
359
+ .passthrough();
360
+ const modelOptionsWithGetterSchema = z
361
+ .object(Object.fromEntries(Object.entries(modelOptionsSchemaProperties).map(([key, schema]) => [
349
362
  key,
350
363
  optionalize(getterSchema(schema)),
351
- ])));
364
+ ])))
365
+ .passthrough();
352
366
  const chatModelOptionsSchema = agentOptionsSchema.extend({
353
367
  model: optionalize(z.string()),
354
368
  modelOptions: optionalize(modelOptionsWithGetterSchema),
@@ -1,21 +1,17 @@
1
1
  import type { AgentInvokeOptions, AgentOptions, Message } from "../../../agents/agent.js";
2
2
  import { AFSSkillBase } from "./base.js";
3
- export interface Patch {
4
- start_line: number;
5
- end_line: number;
6
- replace?: string;
7
- delete: boolean;
8
- }
9
3
  export interface AFSEditInput extends Message {
10
4
  path: string;
11
- patches: Patch[];
5
+ oldString: string;
6
+ newString: string;
7
+ replaceAll?: boolean;
12
8
  }
13
9
  export interface AFSEditOutput extends Message {
14
10
  status: string;
15
11
  tool: string;
16
12
  path: string;
17
13
  message: string;
18
- data: string;
14
+ snippet: string;
19
15
  }
20
16
  export interface AFSEditAgentOptions extends AgentOptions<AFSEditInput, AFSEditOutput> {
21
17
  afs: NonNullable<AgentOptions<AFSEditInput, AFSEditOutput>["afs"]>;
@@ -23,5 +19,6 @@ export interface AFSEditAgentOptions extends AgentOptions<AFSEditInput, AFSEditO
23
19
  export declare class AFSEditAgent extends AFSSkillBase<AFSEditInput, AFSEditOutput> {
24
20
  constructor(options: AFSEditAgentOptions);
25
21
  process(input: AFSEditInput, _options: AgentInvokeOptions): Promise<AFSEditOutput>;
26
- applyCustomPatches(text: string, patches: Patch[]): string;
22
+ private countOccurrences;
23
+ private extractSnippet;
27
24
  }
@@ -1,117 +1,120 @@
1
1
  import { z } from "zod";
2
2
  import { AFSSkillBase } from "./base.js";
3
+ const CONTEXT_LINES = 4; // Number of lines to show before and after the edit
3
4
  export class AFSEditAgent extends AFSSkillBase {
4
5
  constructor(options) {
5
6
  super({
6
7
  name: "afs_edit",
7
- description: `Apply precise line-based patches to modify files in the Agentic File System (AFS)
8
- - Performs targeted edits using line numbers without rewriting the entire file
9
- - Supports both replacing and deleting line ranges
10
- - Multiple patches can be applied in a single operation
8
+ description: `Performs exact string replacements in files within the Agentic File System (AFS).
11
9
 
12
10
  Usage:
11
+ - You must use afs_read at least once before editing to understand the file content
13
12
  - The path must be an absolute AFS path starting with "/" (e.g., "/docs/readme.md")
14
- - This is NOT a local system file path - it operates within the AFS virtual file system
15
- - IMPORTANT: You MUST use afs_read with withLineNumbers=true before editing to get accurate line numbers
16
- - Line numbers are 0-based: first line is 0, second line is 1, etc.
17
- - The range [start_line, end_line) is exclusive on end_line`,
13
+ - Preserve exact indentation (tabs/spaces) as it appears in the file
14
+ - The edit will FAIL if oldString is not found in the file
15
+ - The edit will FAIL if oldString appears multiple times (unless replaceAll is true)
16
+ - Use replaceAll to replace/rename strings across the entire file`,
18
17
  ...options,
19
18
  inputSchema: z.object({
20
19
  path: z
21
20
  .string()
22
21
  .describe("Absolute AFS path to the file to edit (e.g., '/docs/readme.md'). Must start with '/'"),
23
- patches: z
24
- .array(z.object({
25
- start_line: z
26
- .number()
27
- .int()
28
- .describe("Start line number (0-based, inclusive). First line is 0"),
29
- end_line: z
30
- .number()
31
- .int()
32
- .describe("End line number (0-based, exclusive). To edit line 5 only, use start_line=5, end_line=6"),
33
- replace: z
34
- .string()
35
- .optional()
36
- .describe("New content to insert. Omit when delete=true"),
37
- delete: z
38
- .boolean()
39
- .describe("Set to true to delete the line range. Set to false to replace with 'replace' content"),
40
- }))
41
- .min(1)
42
- .describe("Array of patches to apply. Each patch specifies a line range and the operation (delete or replace)"),
22
+ oldString: z
23
+ .string()
24
+ .describe("The exact text to replace. Must match file content exactly including whitespace"),
25
+ newString: z
26
+ .string()
27
+ .describe("The text to replace it with (must be different from oldString)"),
28
+ replaceAll: z
29
+ .boolean()
30
+ .optional()
31
+ .default(false)
32
+ .describe("Replace all occurrences of oldString (default: false)"),
43
33
  }),
44
34
  outputSchema: z.object({
45
35
  status: z.string(),
46
36
  tool: z.string(),
47
37
  path: z.string(),
48
38
  message: z.string(),
49
- data: z.string(),
39
+ snippet: z.string(),
50
40
  }),
51
41
  });
52
42
  }
53
43
  async process(input, _options) {
54
44
  if (!this.afs)
55
45
  throw new Error("AFS is not configured for this agent.");
56
- if (!input.patches?.length) {
57
- throw new Error("No patches provided for afs_edit.");
46
+ const { path, oldString, newString, replaceAll = false } = input;
47
+ if (oldString === newString) {
48
+ throw new Error("oldString and newString must be different");
58
49
  }
59
- const readResult = await this.afs.read(input.path);
50
+ const readResult = await this.afs.read(path);
60
51
  if (!readResult.data?.content || typeof readResult.data.content !== "string") {
61
- throw new Error(`Cannot read file content from: ${input.path}`);
52
+ throw new Error(`Cannot read file content from: ${path}`);
62
53
  }
63
54
  const originalContent = readResult.data.content;
64
- const updatedContent = this.applyCustomPatches(originalContent, input.patches);
65
- await this.afs.write(input.path, {
55
+ // Check if oldString exists in the file
56
+ const occurrences = this.countOccurrences(originalContent, oldString);
57
+ if (occurrences === 0) {
58
+ throw new Error(`oldString not found in file: ${path}`);
59
+ }
60
+ if (occurrences > 1 && !replaceAll) {
61
+ throw new Error(`oldString appears ${occurrences} times in file. Use replaceAll=true to replace all occurrences, or provide more context to make oldString unique.`);
62
+ }
63
+ // Find the position of the first occurrence for snippet extraction
64
+ const firstOccurrenceIndex = originalContent.indexOf(oldString);
65
+ // Perform the replacement
66
+ const updatedContent = replaceAll
67
+ ? originalContent.split(oldString).join(newString)
68
+ : originalContent.replace(oldString, newString);
69
+ await this.afs.write(path, {
66
70
  content: updatedContent,
67
71
  });
72
+ // Generate snippet around the edit location
73
+ const snippet = this.extractSnippet(updatedContent, firstOccurrenceIndex, newString.length);
74
+ const replacementCount = replaceAll ? occurrences : 1;
68
75
  return {
69
76
  status: "success",
70
77
  tool: "afs_edit",
71
- path: input.path,
72
- message: `Applied ${input.patches.length} patches to ${input.path}`,
73
- data: updatedContent,
78
+ path,
79
+ message: `Replaced ${replacementCount} occurrence${replacementCount > 1 ? "s" : ""} in ${path}`,
80
+ snippet,
74
81
  };
75
82
  }
76
- applyCustomPatches(text, patches) {
77
- // Sort by start_line to ensure sequential application
78
- const sorted = [...patches].sort((a, b) => a.start_line - b.start_line);
79
- const lines = text.split("\n");
80
- for (let i = 0; i < sorted.length; i++) {
81
- const patch = sorted[i];
82
- if (!patch)
83
- continue;
84
- const start = patch.start_line;
85
- const end = patch.end_line;
86
- const deleteCount = end - start; // [start, end) range
87
- let delta = 0;
88
- if (patch.delete) {
89
- // Delete mode: remove the specified lines [start, end)
90
- lines.splice(start, deleteCount);
91
- delta = -deleteCount;
92
- }
93
- else {
94
- // Replace mode: replace the specified lines with new content
95
- const replaceLines = patch.replace ? patch.replace.split("\n") : [];
96
- lines.splice(start, deleteCount, ...replaceLines);
97
- delta = replaceLines.length - deleteCount;
98
- }
99
- // Update subsequent patches' line numbers
100
- // For exclusive-end semantics [start, end), we adjust patches that start >= current patch's start_line
101
- // after the current patch has been applied
102
- if (delta !== 0) {
103
- for (let j = i + 1; j < sorted.length; j++) {
104
- const next = sorted[j];
105
- if (!next)
106
- continue;
107
- // Adjust patches that start at or after the current patch's end line
108
- if (next.start_line >= patch.end_line) {
109
- next.start_line += delta;
110
- next.end_line += delta;
111
- }
112
- }
83
+ countOccurrences(text, search) {
84
+ let count = 0;
85
+ let position = text.indexOf(search);
86
+ while (position !== -1) {
87
+ count++;
88
+ position = text.indexOf(search, position + search.length);
89
+ }
90
+ return count;
91
+ }
92
+ extractSnippet(content, editStartIndex, newStringLength) {
93
+ const lines = content.split("\n");
94
+ // Find the line number where the edit starts
95
+ let charCount = 0;
96
+ let editStartLine = 0;
97
+ for (let i = 0; i < lines.length; i++) {
98
+ const lineLength = (lines[i]?.length ?? 0) + 1; // +1 for newline
99
+ if (charCount + lineLength > editStartIndex) {
100
+ editStartLine = i;
101
+ break;
113
102
  }
103
+ charCount += lineLength;
114
104
  }
115
- return lines.join("\n");
105
+ // Calculate how many lines the new content spans
106
+ const newContentLines = content
107
+ .substring(editStartIndex, editStartIndex + newStringLength)
108
+ .split("\n").length;
109
+ const editEndLine = editStartLine + newContentLines - 1;
110
+ // Extract lines with context
111
+ const startLine = Math.max(0, editStartLine - CONTEXT_LINES);
112
+ const endLine = Math.min(lines.length - 1, editEndLine + CONTEXT_LINES);
113
+ // Format with line numbers (1-based)
114
+ const snippetLines = lines.slice(startLine, endLine + 1).map((line, idx) => {
115
+ const lineNum = startLine + idx + 1;
116
+ return `${String(lineNum).padStart(4)}| ${line}`;
117
+ });
118
+ return snippetLines.join("\n");
116
119
  }
117
120
  }
@@ -3,15 +3,19 @@ import type { AgentInvokeOptions, AgentOptions, Message } from "../../../agents/
3
3
  import { AFSSkillBase } from "./base.js";
4
4
  export interface AFSReadInput extends Message {
5
5
  path: string;
6
- withLineNumbers?: boolean;
6
+ offset?: number;
7
+ limit?: number;
7
8
  }
8
9
  export interface AFSReadOutput extends Message {
9
10
  status: string;
10
11
  tool: string;
11
12
  path: string;
12
- withLineNumbers?: boolean;
13
13
  data?: AFSEntry;
14
14
  message?: string;
15
+ totalLines?: number;
16
+ returnedLines?: number;
17
+ truncated?: boolean;
18
+ offset?: number;
15
19
  }
16
20
  export interface AFSReadAgentOptions extends AgentOptions<AFSReadInput, AFSReadOutput> {
17
21
  afs: NonNullable<AgentOptions<AFSReadInput, AFSReadOutput>["afs"]>;
@@ -1,36 +1,50 @@
1
1
  import { z } from "zod";
2
2
  import { AFSSkillBase } from "./base.js";
3
+ const DEFAULT_LINE_LIMIT = 2000;
4
+ const MAX_LINE_LENGTH = 2000;
3
5
  export class AFSReadAgent extends AFSSkillBase {
4
6
  constructor(options) {
5
7
  super({
6
8
  name: "afs_read",
7
9
  description: `Read file contents from the Agentic File System (AFS)
8
- - Returns the complete content of a file at the specified AFS path
9
- - Supports line numbers output for precise editing references
10
- - Use this tool when you need to review, analyze, or understand file content
10
+ - Returns the content of a file at the specified AFS path
11
+ - By default reads up to ${DEFAULT_LINE_LIMIT} lines, use offset/limit for large files
12
+ - Lines longer than ${MAX_LINE_LENGTH} characters will be truncated
11
13
 
12
14
  Usage:
13
- - The path must be an absolute AFS path starting with "/" (e.g., "/docs/readme.md", "/memory/user/notes")
14
- - This is NOT a local system file path - it operates within the AFS virtual file system
15
- - IMPORTANT: You MUST set withLineNumbers to true before using afs_edit, as line numbers are required for precise edits
16
- - Returns the file's content along with metadata (id, path, timestamps, etc.)`,
15
+ - The path must be an absolute AFS path starting with "/" (e.g., "/docs/readme.md")
16
+ - Use offset to start reading from a specific line (0-based)
17
+ - Use limit to control number of lines returned (default: ${DEFAULT_LINE_LIMIT})
18
+ - Check truncated field to know if file was partially returned`,
17
19
  ...options,
18
20
  inputSchema: z.object({
19
21
  path: z
20
22
  .string()
21
23
  .describe("Absolute AFS path to the file to read (e.g., '/docs/readme.md'). Must start with '/'"),
22
- withLineNumbers: z
23
- .boolean()
24
+ offset: z
25
+ .number()
26
+ .int()
27
+ .min(0)
24
28
  .optional()
25
- .describe("MUST be set to true before using afs_edit. Adds line numbers to output (format: '1| line content')"),
29
+ .describe("Line number to start reading from (0-based, default: 0)"),
30
+ limit: z
31
+ .number()
32
+ .int()
33
+ .min(1)
34
+ .max(DEFAULT_LINE_LIMIT)
35
+ .optional()
36
+ .describe(`Maximum number of lines to read (default: ${DEFAULT_LINE_LIMIT})`),
26
37
  }),
27
38
  outputSchema: z.object({
28
39
  status: z.string(),
29
40
  tool: z.string(),
30
41
  path: z.string(),
31
- withLineNumbers: z.boolean().optional(),
32
42
  data: z.custom().optional(),
33
43
  message: z.string().optional(),
44
+ totalLines: z.number().optional(),
45
+ returnedLines: z.number().optional(),
46
+ truncated: z.boolean().optional(),
47
+ offset: z.number().optional(),
34
48
  }),
35
49
  });
36
50
  }
@@ -38,22 +52,44 @@ Usage:
38
52
  if (!this.afs)
39
53
  throw new Error("AFS is not configured for this agent.");
40
54
  const result = await this.afs.read(input.path);
41
- let content = result.data?.content;
42
- if (input.withLineNumbers && typeof content === "string") {
43
- content = content
44
- .split("\n")
45
- .map((line, idx) => `${idx + 1}| ${line}`)
46
- .join("\n");
55
+ if (!result.data?.content || typeof result.data.content !== "string") {
56
+ return {
57
+ status: "success",
58
+ tool: "afs_read",
59
+ path: input.path,
60
+ ...result,
61
+ };
62
+ }
63
+ const offset = input.offset ?? 0;
64
+ const limit = input.limit ?? DEFAULT_LINE_LIMIT;
65
+ const allLines = result.data.content.split("\n");
66
+ const totalLines = allLines.length;
67
+ // Apply offset and limit
68
+ const selectedLines = allLines.slice(offset, offset + limit);
69
+ // Truncate long lines
70
+ const processedLines = selectedLines.map((line) => line.length > MAX_LINE_LENGTH ? `${line.substring(0, MAX_LINE_LENGTH)}... [truncated]` : line);
71
+ const returnedLines = processedLines.length;
72
+ const truncated = offset > 0 || offset + limit < totalLines;
73
+ const processedContent = processedLines.join("\n");
74
+ let message;
75
+ if (truncated) {
76
+ const startLine = offset + 1;
77
+ const endLine = offset + returnedLines;
78
+ message = `Showing lines ${startLine}-${endLine} of ${totalLines}. Use offset/limit to read more.`;
47
79
  }
48
80
  return {
49
81
  status: "success",
50
82
  tool: "afs_read",
51
83
  path: input.path,
52
- withLineNumbers: input.withLineNumbers,
84
+ totalLines,
85
+ returnedLines,
86
+ truncated,
87
+ offset,
88
+ message,
53
89
  ...result,
54
- data: result.data && {
90
+ data: {
55
91
  ...result.data,
56
- content,
92
+ content: processedContent,
57
93
  },
58
94
  };
59
95
  }
@@ -40,7 +40,7 @@ Usage:
40
40
  async process(input, _options) {
41
41
  if (!this.afs)
42
42
  throw new Error("AFS is not configured for this agent.");
43
- const result = await this.afs.write(input.path, {
43
+ const _result = await this.afs.write(input.path, {
44
44
  content: input.content,
45
45
  }, {
46
46
  append: input.append ?? false,
@@ -49,7 +49,7 @@ Usage:
49
49
  status: "success",
50
50
  tool: "afs_write",
51
51
  path: input.path,
52
- ...result,
52
+ message: "File written successfully",
53
53
  };
54
54
  }
55
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/core",
3
- "version": "1.72.0-beta.4",
3
+ "version": "1.72.0-beta.6",
4
4
  "description": "The functional core of agentic AI",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -93,10 +93,10 @@
93
93
  "zod": "^3.25.67",
94
94
  "zod-from-json-schema": "^0.0.5",
95
95
  "zod-to-json-schema": "^3.24.6",
96
- "@aigne/afs-history": "^1.2.0-beta.3",
97
- "@aigne/observability-api": "^0.11.14-beta.1",
98
96
  "@aigne/afs": "^1.4.0-beta.3",
99
- "@aigne/platform-helpers": "^0.6.7-beta"
97
+ "@aigne/afs-history": "^1.2.0-beta.3",
98
+ "@aigne/platform-helpers": "^0.6.7-beta",
99
+ "@aigne/observability-api": "^0.11.14-beta.1"
100
100
  },
101
101
  "devDependencies": {
102
102
  "@types/bun": "^1.2.22",