@robota-sdk/agent-tools 3.0.0-beta.62 → 3.0.0-beta.64
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/browser/browser.d.ts +176 -192
- package/dist/browser/browser.d.ts.map +1 -0
- package/dist/browser/browser.js +2 -1
- package/dist/browser/browser.js.map +1 -0
- package/dist/node/index.cjs +1534 -1663
- package/dist/node/index.d.ts +300 -304
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +1512 -1657
- package/dist/node/index.js.map +1 -0
- package/package.json +7 -7
- package/dist/node/index.d.cts +0 -504
package/dist/node/index.cjs
CHANGED
|
@@ -1,1793 +1,1664 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let node_fs_promises = require("node:fs/promises");
|
|
25
|
+
let node_path = require("node:path");
|
|
26
|
+
let _robota_sdk_agent_core = require("@robota-sdk/agent-core");
|
|
27
|
+
let node_child_process = require("node:child_process");
|
|
28
|
+
let zod = require("zod");
|
|
29
|
+
let node_crypto = require("node:crypto");
|
|
30
|
+
let fast_glob = require("fast-glob");
|
|
31
|
+
fast_glob = __toESM(fast_glob, 1);
|
|
32
|
+
//#region src/sandbox/e2b-sandbox-client.ts
|
|
16
33
|
var E2BSandboxClient = class {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (this.sandbox.sandboxId === snapshotId && this.sandbox.connect) {
|
|
73
|
-
this.sandbox = await this.sandbox.connect();
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
throw new Error(
|
|
77
|
-
"E2B sandbox restore requires connectSandbox(snapshotId) or sandbox.connect()."
|
|
78
|
-
);
|
|
79
|
-
}
|
|
34
|
+
sandbox;
|
|
35
|
+
connectSandbox;
|
|
36
|
+
createSandboxFromSnapshot;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.sandbox = options.sandbox;
|
|
39
|
+
this.connectSandbox = options.connectSandbox;
|
|
40
|
+
this.createSandboxFromSnapshot = options.createSandboxFromSnapshot;
|
|
41
|
+
}
|
|
42
|
+
async run(command, options) {
|
|
43
|
+
const result = await this.sandbox.commands.run(command, {
|
|
44
|
+
background: false,
|
|
45
|
+
timeoutMs: options?.timeoutMs,
|
|
46
|
+
cwd: options?.workingDirectory
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
stdout: result.stdout ?? "",
|
|
50
|
+
stderr: result.stderr ?? "",
|
|
51
|
+
exitCode: result.exitCode ?? result.exit_code ?? 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async readFile(path) {
|
|
55
|
+
const content = await this.sandbox.files.read(path);
|
|
56
|
+
return typeof content === "string" ? content : Buffer.from(content).toString("utf8");
|
|
57
|
+
}
|
|
58
|
+
async writeFile(path, content) {
|
|
59
|
+
await this.sandbox.files.write(path, content);
|
|
60
|
+
}
|
|
61
|
+
async snapshot() {
|
|
62
|
+
if (this.sandbox.createSnapshot) {
|
|
63
|
+
const snapshot = await this.sandbox.createSnapshot();
|
|
64
|
+
const snapshotId = snapshot.snapshotId ?? snapshot.id;
|
|
65
|
+
if (!snapshotId) throw new Error("E2B createSnapshot() did not return a snapshot id.");
|
|
66
|
+
return snapshotId;
|
|
67
|
+
}
|
|
68
|
+
const sandboxId = this.sandbox.sandboxId;
|
|
69
|
+
if (!sandboxId) throw new Error("E2B sandboxId is required to create a resumable sandbox snapshot.");
|
|
70
|
+
if (!this.sandbox.pause) throw new Error("E2B sandbox adapter does not expose pause().");
|
|
71
|
+
await this.sandbox.pause();
|
|
72
|
+
return sandboxId;
|
|
73
|
+
}
|
|
74
|
+
async restore(snapshotId) {
|
|
75
|
+
if (this.createSandboxFromSnapshot) {
|
|
76
|
+
this.sandbox = await this.createSandboxFromSnapshot(snapshotId);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (this.connectSandbox) {
|
|
80
|
+
this.sandbox = await this.connectSandbox(snapshotId);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (this.sandbox.sandboxId === snapshotId && this.sandbox.connect) {
|
|
84
|
+
this.sandbox = await this.sandbox.connect();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
throw new Error("E2B sandbox restore requires connectSandbox(snapshotId) or sandbox.connect().");
|
|
88
|
+
}
|
|
80
89
|
};
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/sandbox/in-memory-sandbox-client.ts
|
|
83
92
|
var InMemorySandboxClient = class {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.files.set(path, content);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
getFile(path) {
|
|
126
|
-
return this.files.get(path);
|
|
127
|
-
}
|
|
93
|
+
files = /* @__PURE__ */ new Map();
|
|
94
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
95
|
+
runHandler;
|
|
96
|
+
snapshotSequence = 0;
|
|
97
|
+
constructor(options = {}) {
|
|
98
|
+
for (const [path, content] of Object.entries(options.files ?? {})) this.files.set(path, content);
|
|
99
|
+
this.runHandler = options.runHandler;
|
|
100
|
+
}
|
|
101
|
+
async run(command, options) {
|
|
102
|
+
if (this.runHandler) return this.runHandler(command, options, this.files);
|
|
103
|
+
return {
|
|
104
|
+
stdout: "",
|
|
105
|
+
stderr: "",
|
|
106
|
+
exitCode: 0
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async readFile(path) {
|
|
110
|
+
const content = this.files.get(path);
|
|
111
|
+
if (content === void 0) throw new Error(`Sandbox file not found: ${path}`);
|
|
112
|
+
return content;
|
|
113
|
+
}
|
|
114
|
+
async writeFile(path, content) {
|
|
115
|
+
this.files.set(path, content);
|
|
116
|
+
}
|
|
117
|
+
async snapshot() {
|
|
118
|
+
const snapshotId = `snapshot-${++this.snapshotSequence}`;
|
|
119
|
+
this.snapshots.set(snapshotId, new Map(this.files));
|
|
120
|
+
return snapshotId;
|
|
121
|
+
}
|
|
122
|
+
async restore(snapshotId) {
|
|
123
|
+
const snapshot = this.snapshots.get(snapshotId);
|
|
124
|
+
if (!snapshot) throw new Error(`Sandbox snapshot not found: ${snapshotId}`);
|
|
125
|
+
this.files.clear();
|
|
126
|
+
for (const [path, content] of snapshot.entries()) this.files.set(path, content);
|
|
127
|
+
}
|
|
128
|
+
getFile(path) {
|
|
129
|
+
return this.files.get(path);
|
|
130
|
+
}
|
|
128
131
|
};
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/sandbox/workspace-manifest.ts
|
|
134
|
+
const DEFAULT_TARGET_ROOT = "/workspace";
|
|
135
|
+
const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
|
|
136
|
+
const SHELL_QUOTE_PATTERN = /'/g;
|
|
132
137
|
async function applyWorkspaceManifest(sandboxClient, manifest, options = {}) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
await applyManifestEntry(sandboxClient, path, targetPath, targetRoot, entry, options)
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
return { entries: appliedEntries };
|
|
138
|
+
if (sandboxClient.applyManifest) return sandboxClient.applyManifest(manifest, options);
|
|
139
|
+
const targetRoot = normalizeSandboxRoot(options.targetRoot ?? DEFAULT_TARGET_ROOT);
|
|
140
|
+
const appliedEntries = [];
|
|
141
|
+
for (const [rawPath, entry] of Object.entries(manifest.entries)) {
|
|
142
|
+
const path = validateWorkspaceManifestPath(rawPath);
|
|
143
|
+
const targetPath = joinSandboxPath(targetRoot, path);
|
|
144
|
+
appliedEntries.push(await applyManifestEntry(sandboxClient, path, targetPath, targetRoot, entry, options));
|
|
145
|
+
}
|
|
146
|
+
return { entries: appliedEntries };
|
|
146
147
|
}
|
|
147
148
|
function validateWorkspaceManifestPath(path) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const parts = path.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
158
|
-
if (parts.length === 0) {
|
|
159
|
-
throw new Error("workspace manifest path must not resolve to the workspace root");
|
|
160
|
-
}
|
|
161
|
-
if (parts.some((part) => part === "..")) {
|
|
162
|
-
throw new Error("workspace manifest path cannot contain traversal segments");
|
|
163
|
-
}
|
|
164
|
-
const normalizedParts = parts.filter((part) => part !== ".");
|
|
165
|
-
if (normalizedParts.length === 0) {
|
|
166
|
-
throw new Error("workspace manifest path must not resolve to the workspace root");
|
|
167
|
-
}
|
|
168
|
-
return normalizedParts.join("/");
|
|
149
|
+
if (path.length === 0) throw new Error("workspace manifest path must not be empty");
|
|
150
|
+
if (path.includes("\0")) throw new Error("workspace manifest path must not contain NUL bytes");
|
|
151
|
+
if (path.startsWith("/") || path.startsWith("\\") || WINDOWS_ABSOLUTE_PATH_PATTERN.test(path)) throw new Error("workspace manifest path must be workspace-relative");
|
|
152
|
+
const parts = path.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
153
|
+
if (parts.length === 0) throw new Error("workspace manifest path must not resolve to the workspace root");
|
|
154
|
+
if (parts.some((part) => part === "..")) throw new Error("workspace manifest path cannot contain traversal segments");
|
|
155
|
+
const normalizedParts = parts.filter((part) => part !== ".");
|
|
156
|
+
if (normalizedParts.length === 0) throw new Error("workspace manifest path must not resolve to the workspace root");
|
|
157
|
+
return normalizedParts.join("/");
|
|
169
158
|
}
|
|
170
159
|
async function applyManifestEntry(sandboxClient, path, targetPath, targetRoot, entry, options) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return assertUnreachable(entry);
|
|
199
|
-
}
|
|
160
|
+
switch (entry.type) {
|
|
161
|
+
case "file":
|
|
162
|
+
await writeSandboxFile(sandboxClient, targetPath, targetRoot, entry.content);
|
|
163
|
+
return createAppliedEntry(path, entry.type);
|
|
164
|
+
case "dir":
|
|
165
|
+
await createSandboxDirectory(sandboxClient, targetPath);
|
|
166
|
+
return createAppliedEntry(path, entry.type);
|
|
167
|
+
case "localFile":
|
|
168
|
+
await copyLocalFile(sandboxClient, entry.src, targetPath, targetRoot, options);
|
|
169
|
+
return createAppliedEntry(path, entry.type);
|
|
170
|
+
case "localDir":
|
|
171
|
+
await copyLocalDirectory(sandboxClient, entry.src, targetPath, options);
|
|
172
|
+
return createAppliedEntry(path, entry.type);
|
|
173
|
+
case "gitRepo":
|
|
174
|
+
await cloneGitRepository(sandboxClient, entry, targetPath);
|
|
175
|
+
return createAppliedEntry(path, entry.type);
|
|
176
|
+
case "s3Mount":
|
|
177
|
+
case "gcsMount":
|
|
178
|
+
case "r2Mount":
|
|
179
|
+
case "azureBlobMount": return {
|
|
180
|
+
path,
|
|
181
|
+
type: entry.type,
|
|
182
|
+
status: "unsupported",
|
|
183
|
+
message: `${entry.type} requires a provider-specific sandbox adapter.`
|
|
184
|
+
};
|
|
185
|
+
default: return assertUnreachable(entry);
|
|
186
|
+
}
|
|
200
187
|
}
|
|
201
188
|
function createAppliedEntry(path, type) {
|
|
202
|
-
|
|
189
|
+
return {
|
|
190
|
+
path,
|
|
191
|
+
type,
|
|
192
|
+
status: "applied"
|
|
193
|
+
};
|
|
203
194
|
}
|
|
204
195
|
async function copyLocalFile(sandboxClient, source, targetPath, targetRoot, options) {
|
|
205
|
-
|
|
206
|
-
const content = await promises.readFile(hostSourcePath, "utf8");
|
|
207
|
-
await writeSandboxFile(sandboxClient, targetPath, targetRoot, content);
|
|
196
|
+
await writeSandboxFile(sandboxClient, targetPath, targetRoot, await (0, node_fs_promises.readFile)(resolveHostSourcePath(source, options.hostRoot), "utf8"));
|
|
208
197
|
}
|
|
209
198
|
async function copyLocalDirectory(sandboxClient, source, targetPath, options) {
|
|
210
|
-
|
|
211
|
-
await copyLocalDirectoryRecursive(sandboxClient, hostSourcePath, targetPath);
|
|
199
|
+
await copyLocalDirectoryRecursive(sandboxClient, resolveHostSourcePath(source, options.hostRoot), targetPath);
|
|
212
200
|
}
|
|
213
201
|
async function copyLocalDirectoryRecursive(sandboxClient, sourcePath, targetPath) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
202
|
+
await createSandboxDirectory(sandboxClient, targetPath);
|
|
203
|
+
const entries = await (0, node_fs_promises.readdir)(sourcePath, { withFileTypes: true });
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
const childSourcePath = (0, node_path.join)(sourcePath, entry.name);
|
|
206
|
+
const childTargetPath = joinSandboxPath(targetPath, entry.name);
|
|
207
|
+
if (entry.isDirectory()) {
|
|
208
|
+
await copyLocalDirectoryRecursive(sandboxClient, childSourcePath, childTargetPath);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (entry.isFile()) {
|
|
212
|
+
const content = await (0, node_fs_promises.readFile)(childSourcePath, "utf8");
|
|
213
|
+
await sandboxClient.writeFile(childTargetPath, content);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
228
216
|
}
|
|
229
217
|
async function cloneGitRepository(sandboxClient, entry, targetPath) {
|
|
230
|
-
|
|
231
|
-
const refArgs = entry.ref ? ` --branch ${quoteShellArg(entry.ref)}` : "";
|
|
232
|
-
await runSandboxCommand(
|
|
233
|
-
sandboxClient,
|
|
234
|
-
`git clone${shallowArgs}${refArgs} ${quoteShellArg(entry.url)} ${quoteShellArg(targetPath)}`
|
|
235
|
-
);
|
|
218
|
+
await runSandboxCommand(sandboxClient, `git clone${entry.shallow === false ? "" : " --depth 1"}${entry.ref ? ` --branch ${quoteShellArg(entry.ref)}` : ""} ${quoteShellArg(entry.url)} ${quoteShellArg(targetPath)}`);
|
|
236
219
|
}
|
|
237
220
|
async function writeSandboxFile(sandboxClient, targetPath, targetRoot, content) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
await sandboxClient.writeFile(targetPath, content);
|
|
221
|
+
const parentPath = node_path.posix.dirname(targetPath);
|
|
222
|
+
if (parentPath !== targetRoot) await createSandboxDirectory(sandboxClient, parentPath);
|
|
223
|
+
await sandboxClient.writeFile(targetPath, content);
|
|
243
224
|
}
|
|
244
225
|
async function createSandboxDirectory(sandboxClient, targetPath) {
|
|
245
|
-
|
|
226
|
+
await runSandboxCommand(sandboxClient, `mkdir -p ${quoteShellArg(targetPath)}`);
|
|
246
227
|
}
|
|
247
228
|
async function runSandboxCommand(sandboxClient, command) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
throw new Error(
|
|
251
|
-
`workspace manifest command failed: ${command}
|
|
252
|
-
${result.stderr ?? result.stdout}`
|
|
253
|
-
);
|
|
254
|
-
}
|
|
229
|
+
const result = await sandboxClient.run(command);
|
|
230
|
+
if (result.exitCode !== 0) throw new Error(`workspace manifest command failed: ${command}\n${result.stderr ?? result.stdout}`);
|
|
255
231
|
}
|
|
256
232
|
function resolveHostSourcePath(source, hostRoot) {
|
|
257
|
-
|
|
233
|
+
return (0, node_path.isAbsolute)(source) ? (0, node_path.resolve)(source) : (0, node_path.resolve)(hostRoot ?? process.cwd(), source);
|
|
258
234
|
}
|
|
259
235
|
function normalizeSandboxRoot(root) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
return normalized.length === 0 ? "/" : normalized;
|
|
236
|
+
const normalized = root.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
237
|
+
if (!normalized.startsWith("/")) throw new Error("workspace manifest targetRoot must be an absolute sandbox path");
|
|
238
|
+
return normalized.length === 0 ? "/" : normalized;
|
|
265
239
|
}
|
|
266
240
|
function joinSandboxPath(root, path) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
return `${normalizedRoot}/${path}`;
|
|
241
|
+
const normalizedRoot = normalizeSandboxRoot(root);
|
|
242
|
+
if (normalizedRoot === "/") return `/${path}`;
|
|
243
|
+
return `${normalizedRoot}/${path}`;
|
|
272
244
|
}
|
|
273
245
|
function quoteShellArg(value) {
|
|
274
|
-
|
|
246
|
+
return `'${value.replace(SHELL_QUOTE_PATTERN, "'\\''")}'`;
|
|
275
247
|
}
|
|
276
248
|
function assertUnreachable(value) {
|
|
277
|
-
|
|
249
|
+
throw new Error(`unsupported workspace manifest entry: ${JSON.stringify(value)}`);
|
|
278
250
|
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/registry/tool-registry.ts
|
|
253
|
+
/**
|
|
254
|
+
* Tool registry implementation
|
|
255
|
+
* Manages tool registration, validation, and retrieval
|
|
256
|
+
*/
|
|
279
257
|
var ToolRegistry = class {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
throw new agentCore.ValidationError(`Parameter "${propName}" must have a type`);
|
|
396
|
-
}
|
|
397
|
-
const validTypes = ["string", "number", "boolean", "array", "object"];
|
|
398
|
-
if (!validTypes.includes(propSchema.type)) {
|
|
399
|
-
throw new agentCore.ValidationError(
|
|
400
|
-
`Parameter "${propName}" has invalid type "${propSchema.type}"`
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
if (schema.parameters.required) {
|
|
406
|
-
const properties = schema.parameters.properties || {};
|
|
407
|
-
for (const requiredField of schema.parameters.required) {
|
|
408
|
-
if (!properties[requiredField]) {
|
|
409
|
-
throw new agentCore.ValidationError(
|
|
410
|
-
`Required parameter "${requiredField}" is not defined in properties`
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
258
|
+
tools = /* @__PURE__ */ new Map();
|
|
259
|
+
/**
|
|
260
|
+
* Register a tool
|
|
261
|
+
*/
|
|
262
|
+
register(tool) {
|
|
263
|
+
if (!tool.schema?.name) throw new _robota_sdk_agent_core.ValidationError("Tool must have a valid schema with name");
|
|
264
|
+
const toolName = tool.schema.name;
|
|
265
|
+
this.validateToolSchema(tool.schema);
|
|
266
|
+
if (this.tools.has(toolName)) _robota_sdk_agent_core.logger.warn(`Tool "${toolName}" is already registered, overriding`, {
|
|
267
|
+
toolName,
|
|
268
|
+
existingTool: this.tools.get(toolName)?.constructor.name
|
|
269
|
+
});
|
|
270
|
+
this.tools.set(toolName, tool);
|
|
271
|
+
_robota_sdk_agent_core.logger.debug(`Tool "${toolName}" registered successfully`, {
|
|
272
|
+
toolName,
|
|
273
|
+
toolType: tool.constructor.name,
|
|
274
|
+
parameters: Object.keys(tool.schema.parameters?.properties || {})
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Unregister a tool
|
|
279
|
+
*/
|
|
280
|
+
unregister(name) {
|
|
281
|
+
if (!this.tools.has(name)) {
|
|
282
|
+
_robota_sdk_agent_core.logger.warn(`Attempted to unregister non-existent tool "${name}"`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
this.tools.delete(name);
|
|
286
|
+
_robota_sdk_agent_core.logger.debug(`Tool "${name}" unregistered successfully`);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Get tool by name
|
|
290
|
+
*/
|
|
291
|
+
get(name) {
|
|
292
|
+
return this.tools.get(name);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Get all registered tools
|
|
296
|
+
*/
|
|
297
|
+
getAll() {
|
|
298
|
+
return Array.from(this.tools.values());
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get tool schemas
|
|
302
|
+
*/
|
|
303
|
+
getSchemas() {
|
|
304
|
+
const tools = this.getAll();
|
|
305
|
+
_robota_sdk_agent_core.logger.debug("[TOOL-FLOW] ToolRegistry.getSchemas() - Tools before schema extraction", {
|
|
306
|
+
count: tools.length,
|
|
307
|
+
tools: tools.map((t) => ({
|
|
308
|
+
name: t.schema?.name ?? "unnamed",
|
|
309
|
+
hasSchema: !!t.schema,
|
|
310
|
+
schemaType: typeof t.schema,
|
|
311
|
+
toolType: t.constructor?.name || "unknown"
|
|
312
|
+
}))
|
|
313
|
+
});
|
|
314
|
+
return this.getAll().map((tool) => tool.schema);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Check if tool exists
|
|
318
|
+
*/
|
|
319
|
+
has(name) {
|
|
320
|
+
return this.tools.has(name);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Clear all tools
|
|
324
|
+
*/
|
|
325
|
+
clear() {
|
|
326
|
+
const toolCount = this.tools.size;
|
|
327
|
+
this.tools.clear();
|
|
328
|
+
_robota_sdk_agent_core.logger.debug(`Cleared ${toolCount} tools from registry`);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get tool names
|
|
332
|
+
*/
|
|
333
|
+
getToolNames() {
|
|
334
|
+
return Array.from(this.tools.keys());
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Get tools by pattern
|
|
338
|
+
*/
|
|
339
|
+
getToolsByPattern(pattern) {
|
|
340
|
+
const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
|
|
341
|
+
return this.getAll().filter((tool) => regex.test(tool.schema.name));
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Get tool count
|
|
345
|
+
*/
|
|
346
|
+
size() {
|
|
347
|
+
return this.tools.size;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Validate tool schema
|
|
351
|
+
*/
|
|
352
|
+
validateToolSchema(schema) {
|
|
353
|
+
if (!schema.name || typeof schema.name !== "string") throw new _robota_sdk_agent_core.ValidationError("Tool schema must have a valid name");
|
|
354
|
+
if (!schema.description || typeof schema.description !== "string") throw new _robota_sdk_agent_core.ValidationError("Tool schema must have a description");
|
|
355
|
+
if (!schema.parameters || typeof schema.parameters !== "object" || schema.parameters === null || Array.isArray(schema.parameters)) throw new _robota_sdk_agent_core.ValidationError("Tool schema must have parameters object");
|
|
356
|
+
if (schema.parameters.type !== "object") throw new _robota_sdk_agent_core.ValidationError("Tool parameters type must be \"object\"");
|
|
357
|
+
if (schema.parameters.properties) for (const propName of Object.keys(schema.parameters.properties)) {
|
|
358
|
+
const propSchema = schema.parameters.properties[propName];
|
|
359
|
+
if (!propSchema?.type) throw new _robota_sdk_agent_core.ValidationError(`Parameter "${propName}" must have a type`);
|
|
360
|
+
if (![
|
|
361
|
+
"string",
|
|
362
|
+
"number",
|
|
363
|
+
"boolean",
|
|
364
|
+
"array",
|
|
365
|
+
"object"
|
|
366
|
+
].includes(propSchema.type)) throw new _robota_sdk_agent_core.ValidationError(`Parameter "${propName}" has invalid type "${propSchema.type}"`);
|
|
367
|
+
}
|
|
368
|
+
if (schema.parameters.required) {
|
|
369
|
+
const properties = schema.parameters.properties || {};
|
|
370
|
+
for (const requiredField of schema.parameters.required) if (!properties[requiredField]) throw new _robota_sdk_agent_core.ValidationError(`Required parameter "${requiredField}" is not defined in properties`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
416
373
|
};
|
|
417
|
-
|
|
418
|
-
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/implementations/function-tool/schema-converter.ts
|
|
376
|
+
/**
|
|
377
|
+
* Convert Zod schema to JSON Schema format with safe undefined handling
|
|
378
|
+
*/
|
|
419
379
|
function zodToJsonSchema(schema, options = {}) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
type: "object",
|
|
438
|
-
properties,
|
|
439
|
-
required,
|
|
440
|
-
...(options.allowAdditionalProperties || schemaDef.unknownKeys === "passthrough") && {
|
|
441
|
-
additionalProperties: true
|
|
442
|
-
}
|
|
443
|
-
};
|
|
380
|
+
const properties = {};
|
|
381
|
+
const required = [];
|
|
382
|
+
const schemaDef = schema._def;
|
|
383
|
+
if (!schemaDef) throw new Error("Zod schema is missing _def; cannot convert to JSON schema.");
|
|
384
|
+
if (schemaDef.typeName === "ZodObject" && schemaDef.shape) {
|
|
385
|
+
const shape = typeof schemaDef.shape === "function" ? schemaDef.shape() : schemaDef.shape;
|
|
386
|
+
for (const [key, typeObj] of Object.entries(shape)) {
|
|
387
|
+
properties[key] = convertZodTypeToProperty(typeObj);
|
|
388
|
+
if (isRequiredField(typeObj)) required.push(key);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
type: "object",
|
|
393
|
+
properties,
|
|
394
|
+
required,
|
|
395
|
+
...(options.allowAdditionalProperties || schemaDef.unknownKeys === "passthrough") && { additionalProperties: true }
|
|
396
|
+
};
|
|
444
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Convert individual Zod type to parameter schema with safe undefined handling
|
|
400
|
+
*/
|
|
445
401
|
function convertZodTypeToProperty(typeObj) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
402
|
+
const typeDef = typeObj._def;
|
|
403
|
+
if (!typeDef) throw new Error("Zod type is missing _def; cannot convert to JSON schema.");
|
|
404
|
+
const base = {};
|
|
405
|
+
if (typeDef.description) base.description = typeDef.description;
|
|
406
|
+
switch (typeDef.typeName) {
|
|
407
|
+
case "ZodString": return {
|
|
408
|
+
type: "string",
|
|
409
|
+
...base
|
|
410
|
+
};
|
|
411
|
+
case "ZodNumber": return {
|
|
412
|
+
type: "number",
|
|
413
|
+
...base
|
|
414
|
+
};
|
|
415
|
+
case "ZodBoolean": return {
|
|
416
|
+
type: "boolean",
|
|
417
|
+
...base
|
|
418
|
+
};
|
|
419
|
+
case "ZodArray":
|
|
420
|
+
if (!typeDef.type) throw new Error("ZodArray is missing item type; cannot convert to JSON schema.");
|
|
421
|
+
return {
|
|
422
|
+
type: "array",
|
|
423
|
+
items: convertZodTypeToProperty(typeDef.type),
|
|
424
|
+
...base
|
|
425
|
+
};
|
|
426
|
+
case "ZodObject": return {
|
|
427
|
+
type: "object",
|
|
428
|
+
...base
|
|
429
|
+
};
|
|
430
|
+
case "ZodEnum": {
|
|
431
|
+
const enumValues = typeDef.values;
|
|
432
|
+
if (!enumValues || !Array.isArray(enumValues)) throw new Error("ZodEnum is missing enum values; cannot convert to JSON schema.");
|
|
433
|
+
return {
|
|
434
|
+
type: "string",
|
|
435
|
+
enum: enumValues,
|
|
436
|
+
...base
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
case "ZodOptional":
|
|
440
|
+
if (typeDef.innerType) return {
|
|
441
|
+
...convertZodTypeToProperty(typeDef.innerType),
|
|
442
|
+
...base
|
|
443
|
+
};
|
|
444
|
+
throw new Error("ZodOptional is missing innerType; cannot convert to JSON schema.");
|
|
445
|
+
case "ZodNullable":
|
|
446
|
+
if (typeDef.innerType) return {
|
|
447
|
+
...convertZodTypeToProperty(typeDef.innerType),
|
|
448
|
+
...base
|
|
449
|
+
};
|
|
450
|
+
throw new Error("ZodNullable is missing innerType; cannot convert to JSON schema.");
|
|
451
|
+
case "ZodDefault":
|
|
452
|
+
if (typeDef.innerType) return {
|
|
453
|
+
...convertZodTypeToProperty(typeDef.innerType),
|
|
454
|
+
...base
|
|
455
|
+
};
|
|
456
|
+
throw new Error("ZodDefault is missing innerType; cannot convert to JSON schema.");
|
|
457
|
+
case "ZodRecord":
|
|
458
|
+
if (typeDef.valueType) return {
|
|
459
|
+
type: "object",
|
|
460
|
+
additionalProperties: convertZodTypeToProperty(typeDef.valueType),
|
|
461
|
+
...base
|
|
462
|
+
};
|
|
463
|
+
return {
|
|
464
|
+
type: "object",
|
|
465
|
+
additionalProperties: { type: "string" },
|
|
466
|
+
...base
|
|
467
|
+
};
|
|
468
|
+
default: throw new Error(`Unsupported Zod type: ${String(typeDef.typeName)}`);
|
|
469
|
+
}
|
|
512
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Check if a Zod field is required (not optional or nullable)
|
|
473
|
+
*/
|
|
513
474
|
function isRequiredField(typeObj) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
return typeDef.typeName !== "ZodOptional" && typeDef.typeName !== "ZodNullable" && typeDef.typeName !== "ZodDefault";
|
|
475
|
+
const typeDef = typeObj._def;
|
|
476
|
+
if (!typeDef) throw new Error("Zod schema is missing _def; cannot determine required fields.");
|
|
477
|
+
return typeDef.typeName !== "ZodOptional" && typeDef.typeName !== "ZodNullable" && typeDef.typeName !== "ZodDefault";
|
|
519
478
|
}
|
|
520
|
-
|
|
521
|
-
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/implementations/function-tool/parameter-validator.ts
|
|
481
|
+
/**
|
|
482
|
+
* Validate individual parameter type against its schema.
|
|
483
|
+
* Returns an error string if invalid, undefined if valid.
|
|
484
|
+
*/
|
|
522
485
|
function validateParameterType(key, value, schema) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
case "object":
|
|
554
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
555
|
-
return `Parameter "${key}" must be an object, got ${typeof value}`;
|
|
556
|
-
}
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
if (schema.enum && schema.enum.length > 0) {
|
|
560
|
-
const enumValues = schema.enum;
|
|
561
|
-
let isValidEnum = false;
|
|
562
|
-
for (const enumValue of enumValues) {
|
|
563
|
-
if (value === enumValue) {
|
|
564
|
-
isValidEnum = true;
|
|
565
|
-
break;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
if (!isValidEnum) {
|
|
569
|
-
return `Parameter "${key}" must be one of: ${enumValues.join(", ")}, got ${value}`;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
return void 0;
|
|
486
|
+
switch (schema["type"]) {
|
|
487
|
+
case "string":
|
|
488
|
+
if (typeof value !== "string") return `Parameter "${key}" must be a string, got ${typeof value}`;
|
|
489
|
+
break;
|
|
490
|
+
case "number":
|
|
491
|
+
if (typeof value !== "number" || isNaN(value)) return `Parameter "${key}" must be a number, got ${typeof value}`;
|
|
492
|
+
break;
|
|
493
|
+
case "boolean":
|
|
494
|
+
if (typeof value !== "boolean") return `Parameter "${key}" must be a boolean, got ${typeof value}`;
|
|
495
|
+
break;
|
|
496
|
+
case "array":
|
|
497
|
+
if (!Array.isArray(value)) return `Parameter "${key}" must be an array, got ${typeof value}`;
|
|
498
|
+
if (schema.items) for (let i = 0; i < value.length; i++) {
|
|
499
|
+
const itemError = validateParameterType(`${key}[${i}]`, value[i], schema.items);
|
|
500
|
+
if (itemError) return itemError;
|
|
501
|
+
}
|
|
502
|
+
break;
|
|
503
|
+
case "object":
|
|
504
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return `Parameter "${key}" must be an object, got ${typeof value}`;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
508
|
+
const enumValues = schema.enum;
|
|
509
|
+
let isValidEnum = false;
|
|
510
|
+
for (const enumValue of enumValues) if (value === enumValue) {
|
|
511
|
+
isValidEnum = true;
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
if (!isValidEnum) return `Parameter "${key}" must be one of: ${enumValues.join(", ")}, got ${value}`;
|
|
515
|
+
}
|
|
573
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Collect all validation errors for the given parameters against a schema.
|
|
519
|
+
*/
|
|
574
520
|
function getValidationErrors(parameters, schemaRequired, schemaProperties, additionalProperties) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
const typeError = validateParameterType(key, value, paramSchema);
|
|
596
|
-
if (typeError) {
|
|
597
|
-
errors.push(typeError);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
return errors;
|
|
521
|
+
const errors = [];
|
|
522
|
+
for (const field of schemaRequired) if (!(field in parameters)) errors.push(`Missing required parameter: ${field}`);
|
|
523
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
524
|
+
const paramSchema = schemaProperties[key];
|
|
525
|
+
if (!paramSchema) {
|
|
526
|
+
if (additionalProperties === true) continue;
|
|
527
|
+
if (additionalProperties && typeof additionalProperties === "object") {
|
|
528
|
+
const additionalTypeError = validateParameterType(key, value, additionalProperties);
|
|
529
|
+
if (additionalTypeError) errors.push(additionalTypeError);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
errors.push(`Unknown parameter: ${key}`);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const typeError = validateParameterType(key, value, paramSchema);
|
|
536
|
+
if (typeError) errors.push(typeError);
|
|
537
|
+
}
|
|
538
|
+
return errors;
|
|
601
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* Validate parameters and return a structured result.
|
|
542
|
+
*/
|
|
602
543
|
function validateToolParameters(parameters, schemaRequired, schemaProperties, additionalProperties) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
);
|
|
609
|
-
return {
|
|
610
|
-
isValid: errors.length === 0,
|
|
611
|
-
errors
|
|
612
|
-
};
|
|
544
|
+
const errors = getValidationErrors(parameters, schemaRequired, schemaProperties, additionalProperties);
|
|
545
|
+
return {
|
|
546
|
+
isValid: errors.length === 0,
|
|
547
|
+
errors
|
|
548
|
+
};
|
|
613
549
|
}
|
|
614
|
-
|
|
615
|
-
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/implementations/function-tool.ts
|
|
552
|
+
/**
|
|
553
|
+
* Function tool implementation
|
|
554
|
+
* Wraps a JavaScript function as a tool with schema validation
|
|
555
|
+
*
|
|
556
|
+
* Implements IFunctionTool without extending AbstractTool to avoid
|
|
557
|
+
* circular runtime dependency (tools → agents → tools).
|
|
558
|
+
*/
|
|
616
559
|
var FunctionTool = class {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* Validate tool parameters with detailed result
|
|
695
|
-
*/
|
|
696
|
-
validateParameters(parameters) {
|
|
697
|
-
return validateToolParameters(
|
|
698
|
-
parameters,
|
|
699
|
-
this.schema.parameters.required || [],
|
|
700
|
-
this.schema.parameters.properties || {},
|
|
701
|
-
this.schema.parameters.additionalProperties
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* Get tool description
|
|
706
|
-
*/
|
|
707
|
-
getDescription() {
|
|
708
|
-
return this.schema.description;
|
|
709
|
-
}
|
|
710
|
-
/**
|
|
711
|
-
* Validate constructor inputs
|
|
712
|
-
*/
|
|
713
|
-
validateConstructorInputs() {
|
|
714
|
-
if (!this.schema) {
|
|
715
|
-
throw new agentCore.ValidationError("Tool schema is required");
|
|
716
|
-
}
|
|
717
|
-
if (!this.fn || typeof this.fn !== "function") {
|
|
718
|
-
throw new agentCore.ValidationError("Tool function is required and must be a function");
|
|
719
|
-
}
|
|
720
|
-
if (!this.schema.name) {
|
|
721
|
-
throw new agentCore.ValidationError("Tool schema must have a name");
|
|
722
|
-
}
|
|
723
|
-
}
|
|
560
|
+
schema;
|
|
561
|
+
fn;
|
|
562
|
+
eventService;
|
|
563
|
+
constructor(schema, fn) {
|
|
564
|
+
this.schema = schema;
|
|
565
|
+
this.fn = fn;
|
|
566
|
+
this.validateConstructorInputs();
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Get tool name
|
|
570
|
+
*/
|
|
571
|
+
getName() {
|
|
572
|
+
return this.schema.name;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Set EventService for post-construction injection.
|
|
576
|
+
* Accepts EventService as-is without transformation.
|
|
577
|
+
* Caller is responsible for providing properly configured EventService.
|
|
578
|
+
*/
|
|
579
|
+
setEventService(eventService) {
|
|
580
|
+
this.eventService = eventService;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Execute the function tool
|
|
584
|
+
*/
|
|
585
|
+
async execute(parameters, context) {
|
|
586
|
+
const toolName = this.schema.name;
|
|
587
|
+
if (!this.validate(parameters)) throw new _robota_sdk_agent_core.ValidationError(`Invalid parameters for tool "${toolName}": ${getValidationErrors(parameters, this.schema.parameters.required || [], this.schema.parameters.properties || {}, this.schema.parameters.additionalProperties).join(", ")}`);
|
|
588
|
+
const startTime = Date.now();
|
|
589
|
+
let result;
|
|
590
|
+
try {
|
|
591
|
+
result = await this.fn(parameters, context);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
if (error instanceof _robota_sdk_agent_core.ToolExecutionError || error instanceof _robota_sdk_agent_core.ValidationError) throw error;
|
|
594
|
+
throw new _robota_sdk_agent_core.ToolExecutionError(`Function tool execution failed: ${error instanceof Error ? error.message : String(error)}`, toolName, error instanceof Error ? error : new Error(String(error)), {
|
|
595
|
+
parameterCount: Object.keys(parameters || {}).length,
|
|
596
|
+
hasContext: !!context
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
const executionTime = Date.now() - startTime;
|
|
600
|
+
return {
|
|
601
|
+
success: true,
|
|
602
|
+
data: result,
|
|
603
|
+
metadata: {
|
|
604
|
+
executionTime,
|
|
605
|
+
toolName,
|
|
606
|
+
parameters
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Validate parameters (simple boolean result)
|
|
612
|
+
*/
|
|
613
|
+
validate(parameters) {
|
|
614
|
+
return getValidationErrors(parameters, this.schema.parameters.required || [], this.schema.parameters.properties || {}, this.schema.parameters.additionalProperties).length === 0;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Validate tool parameters with detailed result
|
|
618
|
+
*/
|
|
619
|
+
validateParameters(parameters) {
|
|
620
|
+
return validateToolParameters(parameters, this.schema.parameters.required || [], this.schema.parameters.properties || {}, this.schema.parameters.additionalProperties);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Get tool description
|
|
624
|
+
*/
|
|
625
|
+
getDescription() {
|
|
626
|
+
return this.schema.description;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Validate constructor inputs
|
|
630
|
+
*/
|
|
631
|
+
validateConstructorInputs() {
|
|
632
|
+
if (!this.schema) throw new _robota_sdk_agent_core.ValidationError("Tool schema is required");
|
|
633
|
+
if (!this.fn || typeof this.fn !== "function") throw new _robota_sdk_agent_core.ValidationError("Tool function is required and must be a function");
|
|
634
|
+
if (!this.schema.name) throw new _robota_sdk_agent_core.ValidationError("Tool schema must have a name");
|
|
635
|
+
}
|
|
724
636
|
};
|
|
637
|
+
/**
|
|
638
|
+
* Helper function to create a function tool from a simple function
|
|
639
|
+
*/
|
|
725
640
|
function createFunctionTool(name, description, parameters, fn) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
return new FunctionTool(schema, fn);
|
|
641
|
+
return new FunctionTool({
|
|
642
|
+
name,
|
|
643
|
+
description,
|
|
644
|
+
parameters
|
|
645
|
+
}, fn);
|
|
732
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Helper function to create a function tool from Zod schema
|
|
649
|
+
*/
|
|
733
650
|
function createZodFunctionTool(name, description, zodSchema, fn) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
return typeof result === "string" ? result : JSON.stringify(result);
|
|
747
|
-
};
|
|
748
|
-
return new FunctionTool(schema, wrappedFn);
|
|
651
|
+
const schema = {
|
|
652
|
+
name,
|
|
653
|
+
description,
|
|
654
|
+
parameters: zodToJsonSchema(zodSchema)
|
|
655
|
+
};
|
|
656
|
+
const wrappedFn = async (parameters, context) => {
|
|
657
|
+
const parseResult = zodSchema.safeParse(parameters);
|
|
658
|
+
if (!parseResult.success) throw new _robota_sdk_agent_core.ValidationError(`Zod validation failed: ${parseResult.error}`);
|
|
659
|
+
const result = await fn(parseResult.data || parameters, context);
|
|
660
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
661
|
+
};
|
|
662
|
+
return new FunctionTool(schema, wrappedFn);
|
|
749
663
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/implementations/openapi-schema-converter.ts
|
|
666
|
+
/**
|
|
667
|
+
* HTTP methods to search when scanning OpenAPI paths
|
|
668
|
+
*/
|
|
669
|
+
const HTTP_METHODS = [
|
|
670
|
+
"get",
|
|
671
|
+
"post",
|
|
672
|
+
"put",
|
|
673
|
+
"delete",
|
|
674
|
+
"patch",
|
|
675
|
+
"head",
|
|
676
|
+
"options"
|
|
760
677
|
];
|
|
678
|
+
/**
|
|
679
|
+
* Find an operation in the OpenAPI spec by operationId
|
|
680
|
+
*/
|
|
761
681
|
function findOperation(apiSpec, operationId) {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
682
|
+
for (const [path, pathItem] of Object.entries(apiSpec.paths || {})) {
|
|
683
|
+
if (!pathItem) continue;
|
|
684
|
+
for (const method of HTTP_METHODS) {
|
|
685
|
+
const operation = pathItem[method];
|
|
686
|
+
if (operation?.operationId === operationId) return {
|
|
687
|
+
method,
|
|
688
|
+
path,
|
|
689
|
+
operation
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
772
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Map OpenAPI type to JSON schema type
|
|
696
|
+
*/
|
|
773
697
|
function mapOpenAPIType(type) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
case "array":
|
|
784
|
-
return "array";
|
|
785
|
-
case "object":
|
|
786
|
-
return "object";
|
|
787
|
-
default:
|
|
788
|
-
return "string";
|
|
789
|
-
}
|
|
698
|
+
switch (type) {
|
|
699
|
+
case "string": return "string";
|
|
700
|
+
case "number": return "number";
|
|
701
|
+
case "integer": return "integer";
|
|
702
|
+
case "boolean": return "boolean";
|
|
703
|
+
case "array": return "array";
|
|
704
|
+
case "object": return "object";
|
|
705
|
+
default: return "string";
|
|
706
|
+
}
|
|
790
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* Convert OpenAPI schema to parameter schema
|
|
710
|
+
*/
|
|
791
711
|
function convertOpenAPISchemaToParameterSchema(schema) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
result.maximum = schema.maximum;
|
|
809
|
-
}
|
|
810
|
-
if (schema.pattern) {
|
|
811
|
-
result.pattern = schema.pattern;
|
|
812
|
-
}
|
|
813
|
-
if (schema.format) {
|
|
814
|
-
result.format = schema.format;
|
|
815
|
-
}
|
|
816
|
-
if (schema.default !== void 0) {
|
|
817
|
-
result.default = schema.default;
|
|
818
|
-
}
|
|
819
|
-
if (schema.type === "array" && schema.items) {
|
|
820
|
-
result.items = convertOpenAPISchemaToParameterSchema(schema.items);
|
|
821
|
-
}
|
|
822
|
-
if (schema.type === "object" && schema.properties) {
|
|
823
|
-
result.properties = {};
|
|
824
|
-
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
825
|
-
result.properties[propName] = convertOpenAPISchemaToParameterSchema(propSchema);
|
|
826
|
-
}
|
|
827
|
-
if (schema.required && schema.required.length > 0) {
|
|
828
|
-
result.required = schema.required;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
return result;
|
|
712
|
+
if ("$ref" in schema) return { type: "object" };
|
|
713
|
+
const result = { type: mapOpenAPIType(schema.type) };
|
|
714
|
+
if (schema.description) result.description = schema.description;
|
|
715
|
+
if (schema.enum) result.enum = schema.enum;
|
|
716
|
+
if (schema.minimum !== void 0) result.minimum = schema.minimum;
|
|
717
|
+
if (schema.maximum !== void 0) result.maximum = schema.maximum;
|
|
718
|
+
if (schema.pattern) result.pattern = schema.pattern;
|
|
719
|
+
if (schema.format) result.format = schema.format;
|
|
720
|
+
if (schema.default !== void 0) result.default = schema.default;
|
|
721
|
+
if (schema.type === "array" && schema.items) result.items = convertOpenAPISchemaToParameterSchema(schema.items);
|
|
722
|
+
if (schema.type === "object" && schema.properties) {
|
|
723
|
+
result.properties = {};
|
|
724
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) result.properties[propName] = convertOpenAPISchemaToParameterSchema(propSchema);
|
|
725
|
+
if (schema.required && schema.required.length > 0) result.required = schema.required;
|
|
726
|
+
}
|
|
727
|
+
return result;
|
|
832
728
|
}
|
|
729
|
+
/**
|
|
730
|
+
* Convert OpenAPI parameter object to tool parameter schema
|
|
731
|
+
*/
|
|
833
732
|
function convertOpenAPIParamToSchema(param) {
|
|
834
|
-
|
|
835
|
-
|
|
733
|
+
const schema = param.schema;
|
|
734
|
+
return convertOpenAPISchemaToParameterSchema(schema);
|
|
836
735
|
}
|
|
736
|
+
/**
|
|
737
|
+
* Create a tool schema from an OpenAPI operation specification
|
|
738
|
+
*/
|
|
837
739
|
function createSchemaFromOperation(operationId, opSpec) {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
schemaParams.required = required;
|
|
867
|
-
}
|
|
868
|
-
return {
|
|
869
|
-
name: operationId,
|
|
870
|
-
description: opSpec.summary || opSpec.description || `OpenAPI operation: ${operationId}`,
|
|
871
|
-
parameters: schemaParams
|
|
872
|
-
};
|
|
740
|
+
const properties = {};
|
|
741
|
+
const required = [];
|
|
742
|
+
const params = opSpec.parameters || [];
|
|
743
|
+
for (const param of params) {
|
|
744
|
+
properties[param.name] = convertOpenAPIParamToSchema(param);
|
|
745
|
+
if (param.required) required.push(param.name);
|
|
746
|
+
}
|
|
747
|
+
if (opSpec.requestBody) {
|
|
748
|
+
const jsonContent = opSpec.requestBody.content?.["application/json"];
|
|
749
|
+
if (jsonContent?.schema) {
|
|
750
|
+
const bodySchema = convertOpenAPISchemaToParameterSchema(jsonContent.schema);
|
|
751
|
+
if (bodySchema.type === "object" && bodySchema.properties) {
|
|
752
|
+
Object.assign(properties, bodySchema.properties);
|
|
753
|
+
const schemaWithRequired = bodySchema;
|
|
754
|
+
if (schemaWithRequired.required) required.push(...schemaWithRequired.required);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const schemaParams = {
|
|
759
|
+
type: "object",
|
|
760
|
+
properties
|
|
761
|
+
};
|
|
762
|
+
if (required.length > 0) schemaParams.required = required;
|
|
763
|
+
return {
|
|
764
|
+
name: operationId,
|
|
765
|
+
description: opSpec.summary || opSpec.description || `OpenAPI operation: ${operationId}`,
|
|
766
|
+
parameters: schemaParams
|
|
767
|
+
};
|
|
873
768
|
}
|
|
874
|
-
|
|
875
|
-
|
|
769
|
+
//#endregion
|
|
770
|
+
//#region src/implementations/openapi-tool.ts
|
|
771
|
+
/**
|
|
772
|
+
* OpenAPI tool implementation
|
|
773
|
+
* Executes API calls based on OpenAPI 3.0 specifications
|
|
774
|
+
*
|
|
775
|
+
* Implements ITool without extending AbstractTool to avoid
|
|
776
|
+
* circular runtime dependency (tools → agents → tools).
|
|
777
|
+
*/
|
|
876
778
|
var OpenAPITool = class {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
}
|
|
1031
|
-
body = JSON.stringify(bodyParams);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
if (this.config.auth) {
|
|
1035
|
-
switch (this.config.auth.type) {
|
|
1036
|
-
case "bearer":
|
|
1037
|
-
headers["Authorization"] = `Bearer ${this.config.auth.token}`;
|
|
1038
|
-
break;
|
|
1039
|
-
case "apiKey": {
|
|
1040
|
-
const headerName = this.config.auth.header || "X-API-Key";
|
|
1041
|
-
headers[headerName] = this.config.auth.apiKey || "";
|
|
1042
|
-
break;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
const result = {
|
|
1047
|
-
method,
|
|
1048
|
-
url,
|
|
1049
|
-
headers
|
|
1050
|
-
};
|
|
1051
|
-
if (body !== void 0) {
|
|
1052
|
-
result.body = body;
|
|
1053
|
-
}
|
|
1054
|
-
return result;
|
|
1055
|
-
}
|
|
1056
|
-
/**
|
|
1057
|
-
* Create tool schema from OpenAPI operation specification
|
|
1058
|
-
*/
|
|
1059
|
-
createSchemaFromOpenAPI() {
|
|
1060
|
-
const operation = findOperation(this.apiSpec, this.operationId);
|
|
1061
|
-
if (!operation) {
|
|
1062
|
-
throw new Error(
|
|
1063
|
-
`[STRICT-POLICY][EMITTER-CONTRACT] OpenAPI operation not found: ${this.operationId}. Emitter contract must provide a valid operationId present in the OpenAPI document.`
|
|
1064
|
-
);
|
|
1065
|
-
}
|
|
1066
|
-
return createSchemaFromOperation(this.operationId, operation.operation);
|
|
1067
|
-
}
|
|
779
|
+
schema;
|
|
780
|
+
apiSpec;
|
|
781
|
+
operationId;
|
|
782
|
+
baseURL;
|
|
783
|
+
config;
|
|
784
|
+
eventService;
|
|
785
|
+
constructor(config) {
|
|
786
|
+
this.config = config;
|
|
787
|
+
if (typeof config.spec !== "object" || config.spec === null || typeof config.spec.openapi !== "string" || typeof config.spec.paths !== "object") throw new Error("Invalid OpenAPI spec: must contain \"openapi\" (string) and \"paths\" (object) fields");
|
|
788
|
+
this.apiSpec = config.spec;
|
|
789
|
+
this.operationId = config.operationId;
|
|
790
|
+
this.baseURL = config.baseURL;
|
|
791
|
+
this.schema = this.createSchemaFromOpenAPI();
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Execute the OpenAPI tool
|
|
795
|
+
*/
|
|
796
|
+
async execute(parameters, context) {
|
|
797
|
+
const toolName = this.schema.name;
|
|
798
|
+
const validation = this.validateParameters(parameters);
|
|
799
|
+
if (!validation.isValid) throw new _robota_sdk_agent_core.ValidationError(`Invalid parameters for OpenAPI tool "${toolName}": ${validation.errors.join(", ")}`);
|
|
800
|
+
try {
|
|
801
|
+
const startTime = Date.now();
|
|
802
|
+
return {
|
|
803
|
+
success: true,
|
|
804
|
+
data: await this.executeAPICall(parameters, context),
|
|
805
|
+
metadata: {
|
|
806
|
+
executionTime: Date.now() - startTime,
|
|
807
|
+
toolName,
|
|
808
|
+
operationId: this.operationId,
|
|
809
|
+
baseURL: this.baseURL
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (error instanceof _robota_sdk_agent_core.ToolExecutionError || error instanceof _robota_sdk_agent_core.ValidationError) throw error;
|
|
814
|
+
const safeError = error instanceof Error ? error : new Error(String(error));
|
|
815
|
+
throw new _robota_sdk_agent_core.ToolExecutionError(`OpenAPI tool execution failed: ${safeError.message}`, toolName, safeError, {
|
|
816
|
+
operationId: this.operationId,
|
|
817
|
+
baseURL: this.baseURL,
|
|
818
|
+
parametersCount: Object.keys(parameters).length
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Validate tool parameters
|
|
824
|
+
*/
|
|
825
|
+
validate(parameters) {
|
|
826
|
+
return this.validateParameters(parameters).isValid;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Validate tool parameters with detailed result
|
|
830
|
+
*/
|
|
831
|
+
validateParameters(parameters) {
|
|
832
|
+
const required = this.schema.parameters.required || [];
|
|
833
|
+
const errors = [];
|
|
834
|
+
for (const field of required) if (!(field in parameters)) errors.push(`Missing required parameter: ${field}`);
|
|
835
|
+
return {
|
|
836
|
+
isValid: errors.length === 0,
|
|
837
|
+
errors
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Get tool name
|
|
842
|
+
*/
|
|
843
|
+
getName() {
|
|
844
|
+
return this.schema.name;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Set EventService for post-construction injection.
|
|
848
|
+
*/
|
|
849
|
+
setEventService(eventService) {
|
|
850
|
+
this.eventService = eventService;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Get tool description
|
|
854
|
+
*/
|
|
855
|
+
getDescription() {
|
|
856
|
+
return this.schema.description;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Execute the actual API call
|
|
860
|
+
* @private
|
|
861
|
+
*/
|
|
862
|
+
async executeAPICall(parameters, _context) {
|
|
863
|
+
const operation = findOperation(this.apiSpec, this.operationId);
|
|
864
|
+
if (!operation) throw new Error(`Operation ${this.operationId} not found in OpenAPI spec`);
|
|
865
|
+
this.buildRequestConfig(operation, parameters);
|
|
866
|
+
throw new Error("Not implemented: actual API execution is not yet available");
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Build HTTP request configuration from OpenAPI operation and parameters
|
|
870
|
+
*/
|
|
871
|
+
buildRequestConfig(opInfo, parameters) {
|
|
872
|
+
const { method, path, operation } = opInfo;
|
|
873
|
+
let url = this.baseURL + path;
|
|
874
|
+
const headers = {};
|
|
875
|
+
let body;
|
|
876
|
+
const params = operation.parameters || [];
|
|
877
|
+
for (const param of params) {
|
|
878
|
+
const value = parameters[param.name];
|
|
879
|
+
if (value === void 0 && param.required) throw new Error(`Required parameter ${param.name} is missing`);
|
|
880
|
+
if (value !== void 0) switch (param.in) {
|
|
881
|
+
case "path":
|
|
882
|
+
url = url.replace(`{${param.name}}`, encodeURIComponent(String(value)));
|
|
883
|
+
break;
|
|
884
|
+
case "query": {
|
|
885
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
886
|
+
url += `${separator}${param.name}=${encodeURIComponent(String(value))}`;
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
case "header":
|
|
890
|
+
headers[param.name] = String(value);
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if ([
|
|
895
|
+
"post",
|
|
896
|
+
"put",
|
|
897
|
+
"patch"
|
|
898
|
+
].includes(method) && operation.requestBody) {
|
|
899
|
+
if (operation.requestBody.content?.["application/json"]) {
|
|
900
|
+
headers["Content-Type"] = "application/json";
|
|
901
|
+
const bodyParams = {};
|
|
902
|
+
for (const [key, value] of Object.entries(parameters)) if (!params.some((p) => p.name === key)) bodyParams[key] = value;
|
|
903
|
+
body = JSON.stringify(bodyParams);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (this.config.auth) switch (this.config.auth.type) {
|
|
907
|
+
case "bearer":
|
|
908
|
+
headers["Authorization"] = `Bearer ${this.config.auth.token}`;
|
|
909
|
+
break;
|
|
910
|
+
case "apiKey": {
|
|
911
|
+
const headerName = this.config.auth.header || "X-API-Key";
|
|
912
|
+
headers[headerName] = this.config.auth.apiKey || "";
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const result = {
|
|
917
|
+
method,
|
|
918
|
+
url,
|
|
919
|
+
headers
|
|
920
|
+
};
|
|
921
|
+
if (body !== void 0) result.body = body;
|
|
922
|
+
return result;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Create tool schema from OpenAPI operation specification
|
|
926
|
+
*/
|
|
927
|
+
createSchemaFromOpenAPI() {
|
|
928
|
+
const operation = findOperation(this.apiSpec, this.operationId);
|
|
929
|
+
if (!operation) throw new Error(`[STRICT-POLICY][EMITTER-CONTRACT] OpenAPI operation not found: ${this.operationId}. Emitter contract must provide a valid operationId present in the OpenAPI document.`);
|
|
930
|
+
return createSchemaFromOperation(this.operationId, operation.operation);
|
|
931
|
+
}
|
|
1068
932
|
};
|
|
933
|
+
/**
|
|
934
|
+
* Factory function to create OpenAPI tools from specification
|
|
935
|
+
*/
|
|
1069
936
|
function createOpenAPITool(config) {
|
|
1070
|
-
|
|
937
|
+
return new OpenAPITool(config);
|
|
1071
938
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
939
|
+
//#endregion
|
|
940
|
+
//#region src/builtins/bash-tool.ts
|
|
941
|
+
/**
|
|
942
|
+
* BashTool — execute shell commands via child_process.spawn
|
|
943
|
+
*
|
|
944
|
+
* Returns TToolResult JSON string. Non-zero exit is returned as success:true
|
|
945
|
+
* with exitCode set, matching Claude Code behaviour (the command ran, it just
|
|
946
|
+
* exited non-zero — the LLM can decide what to do with that information).
|
|
947
|
+
*/
|
|
948
|
+
const DEFAULT_TIMEOUT_MS$2 = 12e4;
|
|
949
|
+
const BashSchema = zod.z.object({
|
|
950
|
+
command: zod.z.string().describe("The bash command to execute"),
|
|
951
|
+
timeout: zod.z.number().optional().describe("Optional timeout in milliseconds (max 600000). Default is 120000 (2 minutes)"),
|
|
952
|
+
workingDirectory: zod.z.string().optional().describe("Working directory for the command. Defaults to the current working directory")
|
|
1077
953
|
});
|
|
954
|
+
/**
|
|
955
|
+
* Run a shell command and return stdout + stderr.
|
|
956
|
+
* Resolves with the TToolResult JSON string.
|
|
957
|
+
*/
|
|
1078
958
|
async function runBash(args, options = {}) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
stderr:
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
stderr:
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
exitCode
|
|
1162
|
-
});
|
|
1163
|
-
});
|
|
1164
|
-
});
|
|
959
|
+
const { command, timeout = DEFAULT_TIMEOUT_MS$2, workingDirectory } = args;
|
|
960
|
+
if (options.sandboxClient) try {
|
|
961
|
+
const sandboxResult = await options.sandboxClient.run(command, {
|
|
962
|
+
timeoutMs: timeout,
|
|
963
|
+
workingDirectory
|
|
964
|
+
});
|
|
965
|
+
const result = {
|
|
966
|
+
success: true,
|
|
967
|
+
output: sandboxResult.stderr ? `${sandboxResult.stdout}\nstderr:\n${sandboxResult.stderr}` : sandboxResult.stdout,
|
|
968
|
+
exitCode: sandboxResult.exitCode
|
|
969
|
+
};
|
|
970
|
+
return JSON.stringify(result);
|
|
971
|
+
} catch (err) {
|
|
972
|
+
const result = {
|
|
973
|
+
success: false,
|
|
974
|
+
output: "",
|
|
975
|
+
error: err instanceof Error ? err.message : String(err)
|
|
976
|
+
};
|
|
977
|
+
return JSON.stringify(result);
|
|
978
|
+
}
|
|
979
|
+
return new Promise((resolve) => {
|
|
980
|
+
const stdoutChunks = [];
|
|
981
|
+
const stderrChunks = [];
|
|
982
|
+
let timedOut = false;
|
|
983
|
+
let settled = false;
|
|
984
|
+
const child = (0, node_child_process.spawn)("sh", ["-c", command], {
|
|
985
|
+
cwd: workingDirectory ?? process.cwd(),
|
|
986
|
+
env: process.env,
|
|
987
|
+
stdio: [
|
|
988
|
+
"pipe",
|
|
989
|
+
"pipe",
|
|
990
|
+
"pipe"
|
|
991
|
+
]
|
|
992
|
+
});
|
|
993
|
+
child.stdout.on("data", (chunk) => {
|
|
994
|
+
stdoutChunks.push(chunk);
|
|
995
|
+
});
|
|
996
|
+
child.stderr.on("data", (chunk) => {
|
|
997
|
+
stderrChunks.push(chunk);
|
|
998
|
+
});
|
|
999
|
+
const timer = setTimeout(() => {
|
|
1000
|
+
timedOut = true;
|
|
1001
|
+
child.kill("SIGTERM");
|
|
1002
|
+
settle({
|
|
1003
|
+
success: false,
|
|
1004
|
+
output: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
1005
|
+
error: `Command timed out after ${timeout}ms`
|
|
1006
|
+
});
|
|
1007
|
+
}, timeout);
|
|
1008
|
+
function settle(result) {
|
|
1009
|
+
if (settled) return;
|
|
1010
|
+
settled = true;
|
|
1011
|
+
clearTimeout(timer);
|
|
1012
|
+
resolve(JSON.stringify(result));
|
|
1013
|
+
}
|
|
1014
|
+
child.on("error", (err) => {
|
|
1015
|
+
settle({
|
|
1016
|
+
success: false,
|
|
1017
|
+
output: "",
|
|
1018
|
+
error: err.message
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
child.on("close", (code) => {
|
|
1022
|
+
if (timedOut) {
|
|
1023
|
+
settle({
|
|
1024
|
+
success: false,
|
|
1025
|
+
output: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
1026
|
+
error: `Command timed out after ${timeout}ms`,
|
|
1027
|
+
exitCode: code ?? void 0
|
|
1028
|
+
});
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
1032
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
1033
|
+
const exitCode = code ?? 0;
|
|
1034
|
+
settle({
|
|
1035
|
+
success: true,
|
|
1036
|
+
output: stderr ? `${stdout}\nstderr:\n${stderr}` : stdout,
|
|
1037
|
+
exitCode
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1165
1041
|
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Create a BashTool instance — register with Robota agent tools registry.
|
|
1044
|
+
*/
|
|
1166
1045
|
function createBashTool(options = {}) {
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
BashSchema,
|
|
1171
|
-
async (params) => {
|
|
1172
|
-
return runBash(params, options);
|
|
1173
|
-
}
|
|
1174
|
-
);
|
|
1046
|
+
return createZodFunctionTool("Bash", "Executes a given bash command and returns its output.\n\nThe working directory persists between commands, but shell state does not.\n\nIMPORTANT: Avoid using this tool to run `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands. Instead, use the appropriate dedicated tool:\n - File search: Use Glob (NOT find or ls)\n - Content search: Use Grep (NOT grep or rg)\n - Read files: Use Read (NOT cat/head/tail)\n - Edit files: Use Edit (NOT sed/awk)\n\nFor simple commands, keep the description brief (5-10 words). For complex commands, include enough context to clarify what the command does.\n\nOutput is limited to 30,000 characters. Longer output will be middle-truncated.", BashSchema, async (params) => {
|
|
1047
|
+
return runBash(params, options);
|
|
1048
|
+
});
|
|
1175
1049
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1050
|
+
/**
|
|
1051
|
+
* BashTool instance — register with Robota agent tools registry.
|
|
1052
|
+
*/
|
|
1053
|
+
const bashTool = createBashTool();
|
|
1054
|
+
//#endregion
|
|
1055
|
+
//#region src/builtins/read-tool.ts
|
|
1056
|
+
/**
|
|
1057
|
+
* ReadTool — read a file and return its contents with line numbers (cat -n style).
|
|
1058
|
+
*
|
|
1059
|
+
* Supports offset/limit for partial reads. Detects binary files and refuses to
|
|
1060
|
+
* return their raw bytes. Default limit is 2000 lines.
|
|
1061
|
+
*/
|
|
1062
|
+
const DEFAULT_LIMIT$1 = 2e3;
|
|
1063
|
+
const ReadSchema = zod.z.object({
|
|
1064
|
+
filePath: zod.z.string().describe("The absolute path to the file to read"),
|
|
1065
|
+
offset: zod.z.number().optional().describe("The line number to start reading from (1-based). Only provide if the file is too large to read at once"),
|
|
1066
|
+
limit: zod.z.number().optional().describe(`The number of lines to read (default: ${DEFAULT_LIMIT$1}). Only provide if the file is too large to read at once`)
|
|
1186
1067
|
});
|
|
1068
|
+
/**
|
|
1069
|
+
* Heuristic binary detection: scan the first 8 KB for null bytes.
|
|
1070
|
+
*/
|
|
1187
1071
|
function isBinary(buffer) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1192
|
-
return false;
|
|
1072
|
+
const checkLength = Math.min(buffer.length, 8192);
|
|
1073
|
+
for (let i = 0; i < checkLength; i++) if (buffer[i] === 0) return true;
|
|
1074
|
+
return false;
|
|
1193
1075
|
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Format lines with 1-based line numbers in cat -n style.
|
|
1078
|
+
* Pads line number to the width of the highest line number.
|
|
1079
|
+
*/
|
|
1194
1080
|
function formatWithLineNumbers(lines, startLine) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
}).join("\n");
|
|
1081
|
+
const lastLineNum = startLine + lines.length - 1;
|
|
1082
|
+
const width = String(lastLineNum).length;
|
|
1083
|
+
return lines.map((line, idx) => {
|
|
1084
|
+
return `${String(startLine + idx).padStart(width, " ")}\t${line}`;
|
|
1085
|
+
}).join("\n");
|
|
1201
1086
|
}
|
|
1202
1087
|
function formatReadResult(filePath, content, startLine, limit) {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
const result = {
|
|
1216
|
-
success: true,
|
|
1217
|
-
output: header + output
|
|
1218
|
-
};
|
|
1219
|
-
return JSON.stringify(result);
|
|
1088
|
+
const allLines = content.split("\n");
|
|
1089
|
+
if (allLines[allLines.length - 1] === "") allLines.pop();
|
|
1090
|
+
const zeroBasedStart = startLine - 1;
|
|
1091
|
+
const selectedLines = allLines.slice(zeroBasedStart, zeroBasedStart + limit);
|
|
1092
|
+
const output = formatWithLineNumbers(selectedLines, startLine);
|
|
1093
|
+
const totalLines = allLines.length;
|
|
1094
|
+
const returnedLines = selectedLines.length;
|
|
1095
|
+
const result = {
|
|
1096
|
+
success: true,
|
|
1097
|
+
output: (returnedLines < totalLines ? `[File: ${filePath} (lines ${startLine}-${startLine + returnedLines - 1} of ${totalLines})]\n` : `[File: ${filePath} (${totalLines} lines)]\n`) + output
|
|
1098
|
+
};
|
|
1099
|
+
return JSON.stringify(result);
|
|
1220
1100
|
}
|
|
1221
1101
|
async function readFileTool(args, options = {}) {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
return JSON.stringify(result);
|
|
1274
|
-
}
|
|
1275
|
-
const content = buffer.toString("utf8");
|
|
1276
|
-
return formatReadResult(filePath, content, startLine, limit);
|
|
1102
|
+
const { filePath, offset, limit = DEFAULT_LIMIT$1 } = args;
|
|
1103
|
+
const startLine = offset !== void 0 && offset > 0 ? offset : 1;
|
|
1104
|
+
if (options.sandboxClient) try {
|
|
1105
|
+
return formatReadResult(filePath, await options.sandboxClient.readFile(filePath), startLine, limit);
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
const result = {
|
|
1108
|
+
success: false,
|
|
1109
|
+
output: "",
|
|
1110
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1111
|
+
};
|
|
1112
|
+
return JSON.stringify(result);
|
|
1113
|
+
}
|
|
1114
|
+
let fileStats;
|
|
1115
|
+
try {
|
|
1116
|
+
fileStats = await (0, node_fs_promises.stat)(filePath);
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
const result = {
|
|
1119
|
+
success: false,
|
|
1120
|
+
output: "",
|
|
1121
|
+
error: `File not found: ${filePath}`
|
|
1122
|
+
};
|
|
1123
|
+
return JSON.stringify(result);
|
|
1124
|
+
}
|
|
1125
|
+
if (!fileStats.isFile()) {
|
|
1126
|
+
const result = {
|
|
1127
|
+
success: false,
|
|
1128
|
+
output: "",
|
|
1129
|
+
error: `Path is not a file: ${filePath}`
|
|
1130
|
+
};
|
|
1131
|
+
return JSON.stringify(result);
|
|
1132
|
+
}
|
|
1133
|
+
let buffer;
|
|
1134
|
+
try {
|
|
1135
|
+
buffer = await (0, node_fs_promises.readFile)(filePath);
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
const result = {
|
|
1138
|
+
success: false,
|
|
1139
|
+
output: "",
|
|
1140
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1141
|
+
};
|
|
1142
|
+
return JSON.stringify(result);
|
|
1143
|
+
}
|
|
1144
|
+
if (isBinary(buffer)) {
|
|
1145
|
+
const result = {
|
|
1146
|
+
success: false,
|
|
1147
|
+
output: "",
|
|
1148
|
+
error: `Binary file not supported: ${filePath}`
|
|
1149
|
+
};
|
|
1150
|
+
return JSON.stringify(result);
|
|
1151
|
+
}
|
|
1152
|
+
return formatReadResult(filePath, buffer.toString("utf8"), startLine, limit);
|
|
1277
1153
|
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Create a ReadTool instance — register with Robota agent tools registry.
|
|
1156
|
+
*/
|
|
1278
1157
|
function createReadTool(options = {}) {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
ReadSchema,
|
|
1283
|
-
async (params) => {
|
|
1284
|
-
return readFileTool(params, options);
|
|
1285
|
-
}
|
|
1286
|
-
);
|
|
1158
|
+
return createZodFunctionTool("Read", "Reads a file from the local filesystem.\n\nBy default, reads up to 2000 lines from the beginning of the file. You can optionally specify offset and limit for partial reads.\n\nResults are returned using cat -n format, with line numbers starting at 1.\n\nThe file_path parameter must be an absolute path, not a relative path.", ReadSchema, async (params) => {
|
|
1159
|
+
return readFileTool(params, options);
|
|
1160
|
+
});
|
|
1287
1161
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1162
|
+
/**
|
|
1163
|
+
* ReadTool instance — register with Robota agent tools registry.
|
|
1164
|
+
*/
|
|
1165
|
+
const readTool = createReadTool();
|
|
1166
|
+
//#endregion
|
|
1167
|
+
//#region src/builtins/atomic-file-write.ts
|
|
1168
|
+
const TEMP_RANDOM_BYTES = 6;
|
|
1169
|
+
const PRESERVED_MODE_BITS = 4095;
|
|
1170
|
+
const MISSING_FILE_ERROR_CODE = "ENOENT";
|
|
1292
1171
|
function createTempFilePath(filePath) {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1172
|
+
const dir = (0, node_path.dirname)(filePath);
|
|
1173
|
+
const name = (0, node_path.basename)(filePath);
|
|
1174
|
+
const suffix = (0, node_crypto.randomBytes)(TEMP_RANDOM_BYTES).toString("hex");
|
|
1175
|
+
return (0, node_path.join)(dir, `.${name}.robota-tmp-${process.pid}-${Date.now()}-${suffix}`);
|
|
1297
1176
|
}
|
|
1298
1177
|
async function readExistingMode(filePath) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
}
|
|
1178
|
+
try {
|
|
1179
|
+
return (await (0, node_fs_promises.stat)(filePath)).mode & PRESERVED_MODE_BITS;
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
if (error instanceof Error && hasErrorCode(error, MISSING_FILE_ERROR_CODE)) return void 0;
|
|
1182
|
+
throw error;
|
|
1183
|
+
}
|
|
1306
1184
|
}
|
|
1307
1185
|
function hasErrorCode(error, code) {
|
|
1308
|
-
|
|
1186
|
+
return "code" in error && error.code === code;
|
|
1309
1187
|
}
|
|
1310
1188
|
async function atomicWriteUtf8File(filePath, content) {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
await promises.rm(tempFilePath, { force: true }).catch(() => void 0);
|
|
1323
|
-
throw error;
|
|
1324
|
-
}
|
|
1189
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(filePath), { recursive: true });
|
|
1190
|
+
const existingMode = await readExistingMode(filePath);
|
|
1191
|
+
const tempFilePath = createTempFilePath(filePath);
|
|
1192
|
+
try {
|
|
1193
|
+
await (0, node_fs_promises.writeFile)(tempFilePath, content, "utf8");
|
|
1194
|
+
if (existingMode !== void 0) await (0, node_fs_promises.chmod)(tempFilePath, existingMode);
|
|
1195
|
+
await (0, node_fs_promises.rename)(tempFilePath, filePath);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
await (0, node_fs_promises.rm)(tempFilePath, { force: true }).catch(() => void 0);
|
|
1198
|
+
throw error;
|
|
1199
|
+
}
|
|
1325
1200
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1201
|
+
//#endregion
|
|
1202
|
+
//#region src/builtins/write-tool.ts
|
|
1203
|
+
/**
|
|
1204
|
+
* WriteTool — write content to a file, auto-creating parent directories.
|
|
1205
|
+
*/
|
|
1206
|
+
const WriteSchema = zod.z.object({
|
|
1207
|
+
filePath: zod.z.string().describe("The absolute path to the file to write"),
|
|
1208
|
+
content: zod.z.string().describe("The content to write to the file")
|
|
1331
1209
|
});
|
|
1332
1210
|
async function writeFileTool(args, options = {}) {
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
};
|
|
1351
|
-
return JSON.stringify(result);
|
|
1352
|
-
}
|
|
1211
|
+
const { filePath, content } = args;
|
|
1212
|
+
try {
|
|
1213
|
+
if (options.sandboxClient) await options.sandboxClient.writeFile(filePath, content);
|
|
1214
|
+
else await atomicWriteUtf8File(filePath, content);
|
|
1215
|
+
const result = {
|
|
1216
|
+
success: true,
|
|
1217
|
+
output: `Written ${Buffer.byteLength(content, "utf8")} bytes to ${filePath}`
|
|
1218
|
+
};
|
|
1219
|
+
return JSON.stringify(result);
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
const result = {
|
|
1222
|
+
success: false,
|
|
1223
|
+
output: "",
|
|
1224
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1225
|
+
};
|
|
1226
|
+
return JSON.stringify(result);
|
|
1227
|
+
}
|
|
1353
1228
|
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Create a WriteTool instance — register with Robota agent tools registry.
|
|
1231
|
+
*/
|
|
1354
1232
|
function createWriteTool(options = {}) {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
WriteSchema,
|
|
1359
|
-
async (params) => {
|
|
1360
|
-
return writeFileTool(params, options);
|
|
1361
|
-
}
|
|
1362
|
-
);
|
|
1233
|
+
return createZodFunctionTool("Write", "Writes a file to the local filesystem. This will overwrite an existing file if one exists.\n\nALWAYS prefer the Edit tool for modifying existing files — it only sends the diff. Only use this tool to create new files or for complete rewrites.\n\nNEVER create documentation files (*.md) or README files unless explicitly requested by the user.", WriteSchema, async (params) => {
|
|
1234
|
+
return writeFileTool(params, options);
|
|
1235
|
+
});
|
|
1363
1236
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1237
|
+
/**
|
|
1238
|
+
* WriteTool instance — register with Robota agent tools registry.
|
|
1239
|
+
*/
|
|
1240
|
+
const writeTool = createWriteTool();
|
|
1241
|
+
//#endregion
|
|
1242
|
+
//#region src/builtins/edit-tool.ts
|
|
1243
|
+
/**
|
|
1244
|
+
* EditTool — perform string-replace edits on a file.
|
|
1245
|
+
*
|
|
1246
|
+
* By default, requires the oldString to appear exactly once in the file
|
|
1247
|
+
* (ensuring surgical edits). Pass replaceAll:true to replace all occurrences.
|
|
1248
|
+
*/
|
|
1249
|
+
const EditSchema = zod.z.object({
|
|
1250
|
+
filePath: zod.z.string().describe("The absolute path to the file to modify"),
|
|
1251
|
+
oldString: zod.z.string().describe("The text to replace (must be an exact match of existing content)"),
|
|
1252
|
+
newString: zod.z.string().describe("The text to replace it with (must be different from old_string)"),
|
|
1253
|
+
replaceAll: zod.z.boolean().optional().describe("Replace all occurrences of old_string (default: false). Useful for renaming variables")
|
|
1372
1254
|
});
|
|
1373
1255
|
async function editFileTool(args, options = {}) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
const result = {
|
|
1426
|
-
success: true,
|
|
1427
|
-
output: `Replaced ${count} occurrence(s) in ${filePath}`,
|
|
1428
|
-
startLine
|
|
1429
|
-
};
|
|
1430
|
-
return JSON.stringify(result);
|
|
1256
|
+
const { filePath, oldString, newString, replaceAll = false } = args;
|
|
1257
|
+
let content;
|
|
1258
|
+
try {
|
|
1259
|
+
content = options.sandboxClient ? await options.sandboxClient.readFile(filePath) : await (0, node_fs_promises.readFile)(filePath, "utf8");
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
const result = {
|
|
1262
|
+
success: false,
|
|
1263
|
+
output: "",
|
|
1264
|
+
error: `File not found: ${filePath}`
|
|
1265
|
+
};
|
|
1266
|
+
return JSON.stringify(result);
|
|
1267
|
+
}
|
|
1268
|
+
if (!content.includes(oldString)) {
|
|
1269
|
+
const result = {
|
|
1270
|
+
success: false,
|
|
1271
|
+
output: "",
|
|
1272
|
+
error: `oldString not found in file: ${filePath}`
|
|
1273
|
+
};
|
|
1274
|
+
return JSON.stringify(result);
|
|
1275
|
+
}
|
|
1276
|
+
if (!replaceAll) {
|
|
1277
|
+
if (content.indexOf(oldString) !== content.lastIndexOf(oldString)) {
|
|
1278
|
+
const result = {
|
|
1279
|
+
success: false,
|
|
1280
|
+
output: "",
|
|
1281
|
+
error: `oldString is not unique in file (found ${content.split(oldString).length - 1} occurrences). Provide more context to make it unique, or use replaceAll:true.`
|
|
1282
|
+
};
|
|
1283
|
+
return JSON.stringify(result);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const updated = replaceAll ? content.split(oldString).join(newString) : content.slice(0, content.indexOf(oldString)) + newString + content.slice(content.indexOf(oldString) + oldString.length);
|
|
1287
|
+
try {
|
|
1288
|
+
if (options.sandboxClient) await options.sandboxClient.writeFile(filePath, updated);
|
|
1289
|
+
else await atomicWriteUtf8File(filePath, updated);
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
const result = {
|
|
1292
|
+
success: false,
|
|
1293
|
+
output: "",
|
|
1294
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1295
|
+
};
|
|
1296
|
+
return JSON.stringify(result);
|
|
1297
|
+
}
|
|
1298
|
+
const count = replaceAll ? content.split(oldString).length - 1 : 1;
|
|
1299
|
+
const matchIdx = content.indexOf(oldString);
|
|
1300
|
+
const startLine = matchIdx >= 0 ? content.substring(0, matchIdx).split("\n").length : 1;
|
|
1301
|
+
const result = {
|
|
1302
|
+
success: true,
|
|
1303
|
+
output: `Replaced ${count} occurrence(s) in ${filePath}`,
|
|
1304
|
+
startLine
|
|
1305
|
+
};
|
|
1306
|
+
return JSON.stringify(result);
|
|
1431
1307
|
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Create an EditTool instance — register with Robota agent tools registry.
|
|
1310
|
+
*/
|
|
1432
1311
|
function createEditTool(options = {}) {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
EditSchema,
|
|
1437
|
-
async (params) => {
|
|
1438
|
-
return editFileTool(params, options);
|
|
1439
|
-
}
|
|
1440
|
-
);
|
|
1312
|
+
return createZodFunctionTool("Edit", "Performs exact string replacements in files.\n\nYou must use the Read tool at least once before editing. When editing text from Read output, preserve the exact indentation.\n\nThe edit will FAIL if old_string is not unique in the file. Either provide more surrounding context to make it unique, or use replace_all to change every instance.\n\nALWAYS prefer editing existing files over creating new ones.", EditSchema, async (params) => {
|
|
1313
|
+
return editFileTool(params, options);
|
|
1314
|
+
});
|
|
1441
1315
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1316
|
+
/**
|
|
1317
|
+
* EditTool instance — register with Robota agent tools registry.
|
|
1318
|
+
*/
|
|
1319
|
+
const editTool = createEditTool();
|
|
1320
|
+
//#endregion
|
|
1321
|
+
//#region src/builtins/glob-tool.ts
|
|
1322
|
+
/**
|
|
1323
|
+
* GlobTool — fast file pattern search using fast-glob.
|
|
1324
|
+
*
|
|
1325
|
+
* Excludes node_modules and .git by default.
|
|
1326
|
+
* Results are sorted by modification time (most recently modified first).
|
|
1327
|
+
*/
|
|
1328
|
+
const DEFAULT_MAX_RESULTS = 1e3;
|
|
1329
|
+
const GlobSchema = zod.z.object({
|
|
1330
|
+
pattern: zod.z.string().describe("The glob pattern to match files against (e.g. \"**/*.ts\", \"src/**/*.tsx\")"),
|
|
1331
|
+
path: zod.z.string().optional().describe("The directory to search in. Defaults to the current working directory. Must be a valid directory path if provided"),
|
|
1332
|
+
limit: zod.z.number().optional().describe("Maximum number of results to return (default: 1000). Use a smaller limit to save context space")
|
|
1452
1333
|
});
|
|
1453
1334
|
async function globFileTool(args) {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
output
|
|
1498
|
-
};
|
|
1499
|
-
return JSON.stringify(result);
|
|
1335
|
+
const { pattern, path: basePath } = args;
|
|
1336
|
+
const cwd = basePath ? (0, node_path.resolve)(basePath) : process.cwd();
|
|
1337
|
+
let matches;
|
|
1338
|
+
try {
|
|
1339
|
+
matches = await (0, fast_glob.default)(pattern, {
|
|
1340
|
+
cwd,
|
|
1341
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
1342
|
+
dot: true,
|
|
1343
|
+
absolute: false
|
|
1344
|
+
});
|
|
1345
|
+
} catch (err) {
|
|
1346
|
+
const result = {
|
|
1347
|
+
success: false,
|
|
1348
|
+
output: "",
|
|
1349
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1350
|
+
};
|
|
1351
|
+
return JSON.stringify(result);
|
|
1352
|
+
}
|
|
1353
|
+
const withMtime = await Promise.all(matches.map(async (p) => {
|
|
1354
|
+
const absPath = (0, node_path.resolve)(cwd, p);
|
|
1355
|
+
try {
|
|
1356
|
+
return {
|
|
1357
|
+
path: p,
|
|
1358
|
+
mtime: (await (0, node_fs_promises.stat)(absPath)).mtimeMs
|
|
1359
|
+
};
|
|
1360
|
+
} catch {
|
|
1361
|
+
return {
|
|
1362
|
+
path: p,
|
|
1363
|
+
mtime: 0
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
}));
|
|
1367
|
+
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
1368
|
+
const maxResults = args.limit ?? DEFAULT_MAX_RESULTS;
|
|
1369
|
+
const totalMatches = withMtime.length;
|
|
1370
|
+
const truncated = totalMatches > maxResults;
|
|
1371
|
+
const sorted = (truncated ? withMtime.slice(0, maxResults) : withMtime).map((f) => f.path);
|
|
1372
|
+
let output = sorted.length > 0 ? sorted.join("\n") : "(no matches)";
|
|
1373
|
+
if (truncated) output += `\n\n[Showing ${maxResults} of ${totalMatches} matches. Use limit parameter to see more.]`;
|
|
1374
|
+
return JSON.stringify({
|
|
1375
|
+
success: true,
|
|
1376
|
+
output
|
|
1377
|
+
});
|
|
1500
1378
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1379
|
+
/**
|
|
1380
|
+
* GlobTool instance — register with Robota agent tools registry.
|
|
1381
|
+
*/
|
|
1382
|
+
const globTool = createZodFunctionTool("Glob", "Fast file pattern matching tool that works with any codebase size.\n\nSupports glob patterns like '**/*.js' or 'src/**/*.ts'. Returns matching file paths sorted by modification time.\n\nUse this tool when you need to find files by name patterns. When doing an open-ended search that may require multiple rounds, use the Agent tool instead.\n\nDefault limit is 1000 results. Use the limit parameter if you need fewer results to save context space.", GlobSchema, async (params) => {
|
|
1383
|
+
return globFileTool(params);
|
|
1384
|
+
});
|
|
1385
|
+
//#endregion
|
|
1386
|
+
//#region src/builtins/grep-tool.ts
|
|
1387
|
+
/**
|
|
1388
|
+
* GrepTool — recursive regex content search.
|
|
1389
|
+
*
|
|
1390
|
+
* Supports two output modes:
|
|
1391
|
+
* - files_with_matches (default): return only file paths that contain a match
|
|
1392
|
+
* - content: return matching lines with optional context lines
|
|
1393
|
+
*/
|
|
1394
|
+
const GrepSchema = zod.z.object({
|
|
1395
|
+
pattern: zod.z.string().describe("The regular expression pattern to search for in file contents"),
|
|
1396
|
+
path: zod.z.string().optional().describe("File or directory to search in. Defaults to the current working directory"),
|
|
1397
|
+
glob: zod.z.string().optional().describe("Glob pattern to filter files (e.g. \"*.ts\", \"*.{ts,tsx}\"). Only files matching this pattern will be searched"),
|
|
1398
|
+
contextLines: zod.z.number().optional().describe("Number of context lines to show before and after each match. Only applies when outputMode is \"content\". Default: 0"),
|
|
1399
|
+
outputMode: zod.z.enum(["files_with_matches", "content"]).optional().describe("Output mode: \"files_with_matches\" shows only file paths (default), \"content\" shows matching lines with context")
|
|
1521
1400
|
});
|
|
1401
|
+
/** Convert a simple glob to a RegExp for file name filtering. */
|
|
1522
1402
|
function globToRegex(glob) {
|
|
1523
|
-
|
|
1524
|
-
|
|
1403
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".+").replace(/\*/g, "[^/]*");
|
|
1404
|
+
return new RegExp(`^${escaped}$`);
|
|
1525
1405
|
}
|
|
1406
|
+
/** Check if a file name matches an optional glob filter. */
|
|
1526
1407
|
function matchesGlob(filename, glob) {
|
|
1527
|
-
|
|
1528
|
-
|
|
1408
|
+
if (glob === void 0) return true;
|
|
1409
|
+
return globToRegex(glob).test(filename);
|
|
1529
1410
|
}
|
|
1411
|
+
/** Gather all files under a directory recursively, excluding node_modules/.git. */
|
|
1530
1412
|
async function collectFiles(dirPath, glob) {
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
}
|
|
1557
|
-
await walk(dirPath);
|
|
1558
|
-
return results;
|
|
1413
|
+
const results = [];
|
|
1414
|
+
async function walk(current) {
|
|
1415
|
+
let entryNames;
|
|
1416
|
+
try {
|
|
1417
|
+
entryNames = await (0, node_fs_promises.readdir)(current);
|
|
1418
|
+
} catch {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
for (const name of entryNames) {
|
|
1422
|
+
if (name === "node_modules" || name === ".git") continue;
|
|
1423
|
+
const fullPath = (0, node_path.join)(current, name);
|
|
1424
|
+
let fileStat;
|
|
1425
|
+
try {
|
|
1426
|
+
fileStat = await (0, node_fs_promises.stat)(fullPath);
|
|
1427
|
+
} catch {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
if (fileStat.isDirectory()) await walk(fullPath);
|
|
1431
|
+
else if (fileStat.isFile()) {
|
|
1432
|
+
if (matchesGlob(name, glob)) results.push(fullPath);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
await walk(dirPath);
|
|
1437
|
+
return results;
|
|
1559
1438
|
}
|
|
1439
|
+
/** Search a single file for lines matching the regex. */
|
|
1560
1440
|
function searchFile(content, filePath, regex, contextLines, outputMode) {
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
const sortedIndices = Array.from(includedIndices).sort((a, b) => a - b);
|
|
1580
|
-
let prevIdx;
|
|
1581
|
-
for (const idx of sortedIndices) {
|
|
1582
|
-
if (prevIdx !== void 0 && idx > prevIdx + 1) {
|
|
1583
|
-
outputLines.push("--");
|
|
1584
|
-
}
|
|
1585
|
-
const lineNum = idx + 1;
|
|
1586
|
-
const marker = matchingIndices.includes(idx) ? ":" : "-";
|
|
1587
|
-
outputLines.push(`${filePath}:${lineNum}${marker}${lines[idx]}`);
|
|
1588
|
-
prevIdx = idx;
|
|
1589
|
-
}
|
|
1590
|
-
return outputLines;
|
|
1441
|
+
const lines = content.split("\n");
|
|
1442
|
+
const matchingIndices = [];
|
|
1443
|
+
for (let i = 0; i < lines.length; i++) if (regex.test(lines[i])) matchingIndices.push(i);
|
|
1444
|
+
if (matchingIndices.length === 0) return [];
|
|
1445
|
+
if (outputMode === "files_with_matches") return [filePath];
|
|
1446
|
+
const includedIndices = /* @__PURE__ */ new Set();
|
|
1447
|
+
for (const idx of matchingIndices) for (let c = Math.max(0, idx - contextLines); c <= Math.min(lines.length - 1, idx + contextLines); c++) includedIndices.add(c);
|
|
1448
|
+
const outputLines = [];
|
|
1449
|
+
const sortedIndices = Array.from(includedIndices).sort((a, b) => a - b);
|
|
1450
|
+
let prevIdx;
|
|
1451
|
+
for (const idx of sortedIndices) {
|
|
1452
|
+
if (prevIdx !== void 0 && idx > prevIdx + 1) outputLines.push("--");
|
|
1453
|
+
const lineNum = idx + 1;
|
|
1454
|
+
const marker = matchingIndices.includes(idx) ? ":" : "-";
|
|
1455
|
+
outputLines.push(`${filePath}:${lineNum}${marker}${lines[idx]}`);
|
|
1456
|
+
prevIdx = idx;
|
|
1457
|
+
}
|
|
1458
|
+
return outputLines;
|
|
1591
1459
|
}
|
|
1592
1460
|
async function grepFileTool(args) {
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
} catch {
|
|
1645
|
-
continue;
|
|
1646
|
-
}
|
|
1647
|
-
const fileMatches = searchFile(content, filePath, regex, contextLines, outputMode);
|
|
1648
|
-
allOutputLines.push(...fileMatches);
|
|
1649
|
-
}
|
|
1650
|
-
const result = {
|
|
1651
|
-
success: true,
|
|
1652
|
-
output: allOutputLines.length > 0 ? allOutputLines.join("\n") : "(no matches)"
|
|
1653
|
-
};
|
|
1654
|
-
return JSON.stringify(result);
|
|
1461
|
+
const { pattern, path: searchPath, glob, contextLines = 0, outputMode = "files_with_matches" } = args;
|
|
1462
|
+
const targetPath = searchPath ? (0, node_path.resolve)(searchPath) : process.cwd();
|
|
1463
|
+
let regex;
|
|
1464
|
+
try {
|
|
1465
|
+
regex = new RegExp(pattern);
|
|
1466
|
+
} catch (err) {
|
|
1467
|
+
const result = {
|
|
1468
|
+
success: false,
|
|
1469
|
+
output: "",
|
|
1470
|
+
error: `Invalid regex pattern: ${pattern}`
|
|
1471
|
+
};
|
|
1472
|
+
return JSON.stringify(result);
|
|
1473
|
+
}
|
|
1474
|
+
let targetStat;
|
|
1475
|
+
try {
|
|
1476
|
+
targetStat = await (0, node_fs_promises.stat)(targetPath);
|
|
1477
|
+
} catch {
|
|
1478
|
+
const result = {
|
|
1479
|
+
success: false,
|
|
1480
|
+
output: "",
|
|
1481
|
+
error: `Path not found: ${targetPath}`
|
|
1482
|
+
};
|
|
1483
|
+
return JSON.stringify(result);
|
|
1484
|
+
}
|
|
1485
|
+
let files;
|
|
1486
|
+
if (targetStat.isFile()) files = [targetPath];
|
|
1487
|
+
else files = await collectFiles(targetPath, glob);
|
|
1488
|
+
const allOutputLines = [];
|
|
1489
|
+
for (const filePath of files) {
|
|
1490
|
+
let content;
|
|
1491
|
+
try {
|
|
1492
|
+
const buffer = await (0, node_fs_promises.readFile)(filePath);
|
|
1493
|
+
const checkLen = Math.min(buffer.length, 8192);
|
|
1494
|
+
let hasBinary = false;
|
|
1495
|
+
for (let i = 0; i < checkLen; i++) if (buffer[i] === 0) {
|
|
1496
|
+
hasBinary = true;
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
if (hasBinary) continue;
|
|
1500
|
+
content = buffer.toString("utf8");
|
|
1501
|
+
} catch {
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
const fileMatches = searchFile(content, filePath, regex, contextLines, outputMode);
|
|
1505
|
+
allOutputLines.push(...fileMatches);
|
|
1506
|
+
}
|
|
1507
|
+
const result = {
|
|
1508
|
+
success: true,
|
|
1509
|
+
output: allOutputLines.length > 0 ? allOutputLines.join("\n") : "(no matches)"
|
|
1510
|
+
};
|
|
1511
|
+
return JSON.stringify(result);
|
|
1655
1512
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1513
|
+
/**
|
|
1514
|
+
* GrepTool instance — register with Robota agent tools registry.
|
|
1515
|
+
*/
|
|
1516
|
+
const grepTool = createZodFunctionTool("Grep", "A powerful search tool built on regex matching.\n\nSupports full regex syntax (e.g., 'log.*Error', 'function\\\\s+\\\\w+'). Filter files with glob parameter (e.g., '*.js', '**/*.tsx').\n\nOutput modes: 'content' shows matching lines with context, 'files_with_matches' shows only file paths (default), 'count' shows match counts.\n\nUse this tool for ALL search tasks. NEVER invoke grep or rg as a Bash command.\n\nUse head_limit to control result size and save context space.", GrepSchema, async (params) => {
|
|
1517
|
+
return grepFileTool(params);
|
|
1518
|
+
});
|
|
1519
|
+
//#endregion
|
|
1520
|
+
//#region src/builtins/web-fetch-tool.ts
|
|
1521
|
+
/**
|
|
1522
|
+
* WebFetchTool — fetch a URL and return its content as text.
|
|
1523
|
+
*
|
|
1524
|
+
* HTML is stripped to plain text for readability. Uses Node.js native fetch.
|
|
1525
|
+
* Output is capped at 30K chars (same as other tools).
|
|
1526
|
+
*/
|
|
1527
|
+
const DEFAULT_TIMEOUT_MS$1 = 3e4;
|
|
1528
|
+
const MAX_RESPONSE_BYTES = 5e6;
|
|
1529
|
+
const WebFetchSchema = zod.z.object({
|
|
1530
|
+
url: zod.z.string().describe("The URL to fetch"),
|
|
1531
|
+
headers: zod.z.record(zod.z.string()).optional().describe("Optional HTTP headers as key-value pairs")
|
|
1669
1532
|
});
|
|
1533
|
+
/** Strip HTML tags and decode common entities to produce readable text. */
|
|
1670
1534
|
function htmlToText(html) {
|
|
1671
|
-
|
|
1535
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
1672
1536
|
}
|
|
1673
1537
|
async function runWebFetch(args) {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1538
|
+
const { url, headers } = args;
|
|
1539
|
+
try {
|
|
1540
|
+
new URL(url);
|
|
1541
|
+
} catch {
|
|
1542
|
+
const result = {
|
|
1543
|
+
success: false,
|
|
1544
|
+
output: "",
|
|
1545
|
+
error: `Invalid URL: ${url}`
|
|
1546
|
+
};
|
|
1547
|
+
return JSON.stringify(result);
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
const controller = new AbortController();
|
|
1551
|
+
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS$1);
|
|
1552
|
+
const response = await fetch(url, {
|
|
1553
|
+
headers: {
|
|
1554
|
+
"User-Agent": "Robota-CLI/3.0",
|
|
1555
|
+
...headers ?? {}
|
|
1556
|
+
},
|
|
1557
|
+
signal: controller.signal,
|
|
1558
|
+
redirect: "follow"
|
|
1559
|
+
});
|
|
1560
|
+
clearTimeout(timeout);
|
|
1561
|
+
if (!response.ok) {
|
|
1562
|
+
const result = {
|
|
1563
|
+
success: false,
|
|
1564
|
+
output: "",
|
|
1565
|
+
error: `HTTP ${response.status} ${response.statusText}`
|
|
1566
|
+
};
|
|
1567
|
+
return JSON.stringify(result);
|
|
1568
|
+
}
|
|
1569
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1570
|
+
const buffer = await response.arrayBuffer();
|
|
1571
|
+
if (buffer.byteLength > MAX_RESPONSE_BYTES) {
|
|
1572
|
+
const result = {
|
|
1573
|
+
success: false,
|
|
1574
|
+
output: "",
|
|
1575
|
+
error: `Response too large: ${buffer.byteLength} bytes (max ${MAX_RESPONSE_BYTES})`
|
|
1576
|
+
};
|
|
1577
|
+
return JSON.stringify(result);
|
|
1578
|
+
}
|
|
1579
|
+
let text = new TextDecoder().decode(buffer);
|
|
1580
|
+
if (contentType.includes("html")) text = htmlToText(text);
|
|
1581
|
+
return JSON.stringify({
|
|
1582
|
+
success: true,
|
|
1583
|
+
output: text
|
|
1584
|
+
});
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
const result = {
|
|
1587
|
+
success: false,
|
|
1588
|
+
output: "",
|
|
1589
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1590
|
+
};
|
|
1591
|
+
return JSON.stringify(result);
|
|
1592
|
+
}
|
|
1722
1593
|
}
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1594
|
+
const webFetchTool = createZodFunctionTool("WebFetch", "Fetch a URL and return its content as text. HTML pages are converted to plain text.", WebFetchSchema, async (params) => runWebFetch(params));
|
|
1595
|
+
//#endregion
|
|
1596
|
+
//#region src/builtins/web-search-tool.ts
|
|
1597
|
+
/**
|
|
1598
|
+
* WebSearchTool — search the web and return results.
|
|
1599
|
+
*
|
|
1600
|
+
* Uses Brave Search API when BRAVE_API_KEY is set.
|
|
1601
|
+
* Returns an error with setup instructions otherwise.
|
|
1602
|
+
*/
|
|
1603
|
+
const DEFAULT_LIMIT = 10;
|
|
1604
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
1605
|
+
const WebSearchSchema = zod.z.object({
|
|
1606
|
+
query: zod.z.string().describe("The search query"),
|
|
1607
|
+
limit: zod.z.number().optional().describe(`Maximum number of results to return (default: ${DEFAULT_LIMIT})`)
|
|
1734
1608
|
});
|
|
1735
1609
|
async function runWebSearch(args) {
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1610
|
+
const { query, limit = DEFAULT_LIMIT } = args;
|
|
1611
|
+
const apiKey = process.env["BRAVE_API_KEY"];
|
|
1612
|
+
if (!apiKey) return JSON.stringify({
|
|
1613
|
+
success: false,
|
|
1614
|
+
output: "",
|
|
1615
|
+
error: "Web search requires BRAVE_API_KEY environment variable. Get a free API key at https://brave.com/search/api/ (2,000 queries/month free)."
|
|
1616
|
+
});
|
|
1617
|
+
try {
|
|
1618
|
+
const controller = new AbortController();
|
|
1619
|
+
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
1620
|
+
const params = new URLSearchParams({
|
|
1621
|
+
q: query,
|
|
1622
|
+
count: String(Math.min(limit, 20))
|
|
1623
|
+
});
|
|
1624
|
+
const response = await fetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
|
|
1625
|
+
headers: {
|
|
1626
|
+
Accept: "application/json",
|
|
1627
|
+
"Accept-Encoding": "gzip",
|
|
1628
|
+
"X-Subscription-Token": apiKey
|
|
1629
|
+
},
|
|
1630
|
+
signal: controller.signal
|
|
1631
|
+
});
|
|
1632
|
+
clearTimeout(timeout);
|
|
1633
|
+
if (!response.ok) {
|
|
1634
|
+
const result = {
|
|
1635
|
+
success: false,
|
|
1636
|
+
output: "",
|
|
1637
|
+
error: `Brave Search API error: HTTP ${response.status} ${response.statusText}`
|
|
1638
|
+
};
|
|
1639
|
+
return JSON.stringify(result);
|
|
1640
|
+
}
|
|
1641
|
+
const results = ((await response.json()).web?.results ?? []).map((r) => ({
|
|
1642
|
+
title: r.title,
|
|
1643
|
+
url: r.url,
|
|
1644
|
+
snippet: r.description
|
|
1645
|
+
}));
|
|
1646
|
+
const result = {
|
|
1647
|
+
success: true,
|
|
1648
|
+
output: JSON.stringify(results, null, 2)
|
|
1649
|
+
};
|
|
1650
|
+
return JSON.stringify(result);
|
|
1651
|
+
} catch (err) {
|
|
1652
|
+
const result = {
|
|
1653
|
+
success: false,
|
|
1654
|
+
output: "",
|
|
1655
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1656
|
+
};
|
|
1657
|
+
return JSON.stringify(result);
|
|
1658
|
+
}
|
|
1783
1659
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
"Search the web and return results with title, URL, and snippet.",
|
|
1787
|
-
WebSearchSchema,
|
|
1788
|
-
async (params) => runWebSearch(params)
|
|
1789
|
-
);
|
|
1790
|
-
|
|
1660
|
+
const webSearchTool = createZodFunctionTool("WebSearch", "Search the web and return results with title, URL, and snippet.", WebSearchSchema, async (params) => runWebSearch(params));
|
|
1661
|
+
//#endregion
|
|
1791
1662
|
exports.E2BSandboxClient = E2BSandboxClient;
|
|
1792
1663
|
exports.FunctionTool = FunctionTool;
|
|
1793
1664
|
exports.InMemorySandboxClient = InMemorySandboxClient;
|