@aigne/core 1.72.0-beta.4 → 1.72.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/lib/cjs/prompt/skills/afs/edit.d.ts +6 -9
- package/lib/cjs/prompt/skills/afs/edit.js +79 -76
- package/lib/cjs/prompt/skills/afs/read.d.ts +6 -2
- package/lib/cjs/prompt/skills/afs/read.js +56 -20
- package/lib/cjs/prompt/skills/afs/write.js +2 -2
- package/lib/dts/prompt/skills/afs/edit.d.ts +6 -9
- package/lib/dts/prompt/skills/afs/read.d.ts +6 -2
- package/lib/esm/prompt/skills/afs/edit.d.ts +6 -9
- package/lib/esm/prompt/skills/afs/edit.js +79 -76
- package/lib/esm/prompt/skills/afs/read.d.ts +6 -2
- package/lib/esm/prompt/skills/afs/read.js +56 -20
- package/lib/esm/prompt/skills/afs/write.js +2 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [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)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **core:** afs skills improvements ([#849](https://github.com/AIGNE-io/aigne-framework/issues/849)) ([557cc8b](https://github.com/AIGNE-io/aigne-framework/commit/557cc8b4b72f0e91ad654556f47bbe0ad0ececdb))
|
|
9
|
+
|
|
3
10
|
## [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
11
|
|
|
5
12
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: `
|
|
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
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
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
|
-
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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(
|
|
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: ${
|
|
55
|
+
throw new Error(`Cannot read file content from: ${path}`);
|
|
65
56
|
}
|
|
66
57
|
const originalContent = readResult.data.content;
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
75
|
-
message: `
|
|
76
|
-
|
|
81
|
+
path,
|
|
82
|
+
message: `Replaced ${replacementCount} occurrence${replacementCount > 1 ? "s" : ""} in ${path}`,
|
|
83
|
+
snippet,
|
|
77
84
|
};
|
|
78
85
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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"
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
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
|
-
|
|
26
|
-
.
|
|
27
|
+
offset: zod_1.z
|
|
28
|
+
.number()
|
|
29
|
+
.int()
|
|
30
|
+
.min(0)
|
|
27
31
|
.optional()
|
|
28
|
-
.describe("
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.
|
|
49
|
-
|
|
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
|
-
|
|
87
|
+
totalLines,
|
|
88
|
+
returnedLines,
|
|
89
|
+
truncated,
|
|
90
|
+
offset,
|
|
91
|
+
message,
|
|
56
92
|
...result,
|
|
57
|
-
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: `
|
|
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
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
|
-
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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(
|
|
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: ${
|
|
52
|
+
throw new Error(`Cannot read file content from: ${path}`);
|
|
62
53
|
}
|
|
63
54
|
const originalContent = readResult.data.content;
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
72
|
-
message: `
|
|
73
|
-
|
|
78
|
+
path,
|
|
79
|
+
message: `Replaced ${replacementCount} occurrence${replacementCount > 1 ? "s" : ""} in ${path}`,
|
|
80
|
+
snippet,
|
|
74
81
|
};
|
|
75
82
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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"
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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
|
-
|
|
23
|
-
.
|
|
24
|
+
offset: z
|
|
25
|
+
.number()
|
|
26
|
+
.int()
|
|
27
|
+
.min(0)
|
|
24
28
|
.optional()
|
|
25
|
-
.describe("
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
|
|
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
|
-
|
|
84
|
+
totalLines,
|
|
85
|
+
returnedLines,
|
|
86
|
+
truncated,
|
|
87
|
+
offset,
|
|
88
|
+
message,
|
|
53
89
|
...result,
|
|
54
|
-
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
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.72.0-beta.5",
|
|
4
4
|
"description": "The functional core of agentic AI",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -93,9 +93,9 @@
|
|
|
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",
|
|
97
|
+
"@aigne/observability-api": "^0.11.14-beta.1",
|
|
98
|
+
"@aigne/afs-history": "^1.2.0-beta.3",
|
|
99
99
|
"@aigne/platform-helpers": "^0.6.7-beta"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|