@gugacoder/agentic-sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +463 -0
- package/dist/context/compaction.d.ts +27 -0
- package/dist/context/compaction.js +219 -0
- package/dist/context/models.d.ts +6 -0
- package/dist/context/models.js +41 -0
- package/dist/context/tokenizer.d.ts +5 -0
- package/dist/context/tokenizer.js +11 -0
- package/dist/context/usage.d.ts +11 -0
- package/dist/context/usage.js +49 -0
- package/dist/display-schemas.d.ts +1865 -0
- package/dist/display-schemas.js +219 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +28 -0
- package/dist/middleware/logging.d.ts +2 -0
- package/dist/middleware/logging.js +32 -0
- package/dist/prompts/assembly.d.ts +13 -0
- package/dist/prompts/assembly.js +229 -0
- package/dist/providers.d.ts +19 -0
- package/dist/providers.js +44 -0
- package/dist/proxy.d.ts +2 -0
- package/dist/proxy.js +103 -0
- package/dist/schemas.d.ts +228 -0
- package/dist/schemas.js +51 -0
- package/dist/session.d.ts +7 -0
- package/dist/session.js +102 -0
- package/dist/structured.d.ts +18 -0
- package/dist/structured.js +38 -0
- package/dist/tool-repair.d.ts +21 -0
- package/dist/tool-repair.js +72 -0
- package/dist/tools/api-spec.d.ts +4 -0
- package/dist/tools/api-spec.js +123 -0
- package/dist/tools/apply-patch.d.ts +484 -0
- package/dist/tools/apply-patch.js +157 -0
- package/dist/tools/ask-user.d.ts +14 -0
- package/dist/tools/ask-user.js +27 -0
- package/dist/tools/bash.d.ts +550 -0
- package/dist/tools/bash.js +43 -0
- package/dist/tools/batch.d.ts +13 -0
- package/dist/tools/batch.js +84 -0
- package/dist/tools/brave-search.d.ts +6 -0
- package/dist/tools/brave-search.js +19 -0
- package/dist/tools/code-search.d.ts +20 -0
- package/dist/tools/code-search.js +42 -0
- package/dist/tools/diagnostics.d.ts +4 -0
- package/dist/tools/diagnostics.js +69 -0
- package/dist/tools/display.d.ts +483 -0
- package/dist/tools/display.js +77 -0
- package/dist/tools/edit.d.ts +682 -0
- package/dist/tools/edit.js +47 -0
- package/dist/tools/glob.d.ts +4 -0
- package/dist/tools/glob.js +42 -0
- package/dist/tools/grep.d.ts +6 -0
- package/dist/tools/grep.js +69 -0
- package/dist/tools/http-request.d.ts +7 -0
- package/dist/tools/http-request.js +98 -0
- package/dist/tools/index.d.ts +1611 -0
- package/dist/tools/index.js +46 -0
- package/dist/tools/job-tools.d.ts +24 -0
- package/dist/tools/job-tools.js +67 -0
- package/dist/tools/list-dir.d.ts +5 -0
- package/dist/tools/list-dir.js +79 -0
- package/dist/tools/multi-edit.d.ts +814 -0
- package/dist/tools/multi-edit.js +57 -0
- package/dist/tools/read.d.ts +5 -0
- package/dist/tools/read.js +33 -0
- package/dist/tools/task.d.ts +21 -0
- package/dist/tools/task.js +51 -0
- package/dist/tools/todo.d.ts +14 -0
- package/dist/tools/todo.js +60 -0
- package/dist/tools/web-fetch.d.ts +4 -0
- package/dist/tools/web-fetch.js +126 -0
- package/dist/tools/web-search.d.ts +22 -0
- package/dist/tools/web-search.js +48 -0
- package/dist/tools/write.d.ts +550 -0
- package/dist/tools/write.js +30 -0
- package/dist/types.d.ts +201 -0
- package/dist/types.js +1 -0
- package/package.json +43 -0
- package/src/agent.ts +520 -0
- package/src/context/compaction.ts +265 -0
- package/src/context/models.ts +42 -0
- package/src/context/tokenizer.ts +12 -0
- package/src/context/usage.ts +65 -0
- package/src/display-schemas.ts +276 -0
- package/src/index.ts +43 -0
- package/src/middleware/logging.ts +37 -0
- package/src/prompts/assembly.ts +263 -0
- package/src/prompts/identity.md +10 -0
- package/src/prompts/patterns.md +7 -0
- package/src/prompts/safety.md +7 -0
- package/src/prompts/tool-guide.md +9 -0
- package/src/prompts/tools/bash.md +7 -0
- package/src/prompts/tools/edit.md +7 -0
- package/src/prompts/tools/glob.md +7 -0
- package/src/prompts/tools/grep.md +7 -0
- package/src/prompts/tools/read.md +7 -0
- package/src/prompts/tools/write.md +7 -0
- package/src/providers.ts +58 -0
- package/src/proxy.ts +101 -0
- package/src/schemas.ts +58 -0
- package/src/session.ts +110 -0
- package/src/structured.ts +65 -0
- package/src/tool-repair.ts +92 -0
- package/src/tools/api-spec.ts +158 -0
- package/src/tools/apply-patch.ts +188 -0
- package/src/tools/ask-user.ts +40 -0
- package/src/tools/bash.ts +51 -0
- package/src/tools/batch.ts +103 -0
- package/src/tools/brave-search.ts +24 -0
- package/src/tools/code-search.ts +69 -0
- package/src/tools/diagnostics.ts +93 -0
- package/src/tools/display.ts +105 -0
- package/src/tools/edit.ts +55 -0
- package/src/tools/glob.ts +46 -0
- package/src/tools/grep.ts +68 -0
- package/src/tools/http-request.ts +103 -0
- package/src/tools/index.ts +48 -0
- package/src/tools/job-tools.ts +84 -0
- package/src/tools/list-dir.ts +102 -0
- package/src/tools/multi-edit.ts +65 -0
- package/src/tools/read.ts +40 -0
- package/src/tools/task.ts +71 -0
- package/src/tools/todo.ts +82 -0
- package/src/tools/web-fetch.ts +155 -0
- package/src/tools/web-search.ts +75 -0
- package/src/tools/write.ts +34 -0
- package/src/types.ts +145 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
export function createMultiEditTool(opts) {
|
|
5
|
+
const baseTool = tool({
|
|
6
|
+
description: "Applies multiple string replacements to a single file atomically. If any edit fails (old_string not found), none are applied.",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
file_path: z.string().describe("Absolute path to the file to edit"),
|
|
9
|
+
edits: z
|
|
10
|
+
.array(z.object({
|
|
11
|
+
old_string: z.string().describe("The exact text to find"),
|
|
12
|
+
new_string: z.string().describe("The replacement text"),
|
|
13
|
+
replace_all: z
|
|
14
|
+
.boolean()
|
|
15
|
+
.optional()
|
|
16
|
+
.default(false)
|
|
17
|
+
.describe("Replace all occurrences instead of just the first"),
|
|
18
|
+
}))
|
|
19
|
+
.min(1)
|
|
20
|
+
.describe("Array of edits to apply sequentially"),
|
|
21
|
+
}),
|
|
22
|
+
execute: async ({ file_path, edits }) => {
|
|
23
|
+
try {
|
|
24
|
+
let content = await readFile(file_path, "utf-8");
|
|
25
|
+
// Validation pass: check all old_strings exist before applying any edit
|
|
26
|
+
for (let i = 0; i < edits.length; i++) {
|
|
27
|
+
const edit = edits[i];
|
|
28
|
+
if (!content.includes(edit.old_string)) {
|
|
29
|
+
return `Error: edit[${i}] old_string not found in ${file_path}. No edits applied (atomic rollback).`;
|
|
30
|
+
}
|
|
31
|
+
if (!edit.replace_all) {
|
|
32
|
+
const count = content.split(edit.old_string).length - 1;
|
|
33
|
+
if (count > 1) {
|
|
34
|
+
return `Error: edit[${i}] old_string appears ${count} times. Provide more context or set replace_all to true. No edits applied.`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Apply the edit to the working content so subsequent edits see the result
|
|
38
|
+
content = edit.replace_all
|
|
39
|
+
? content.replaceAll(edit.old_string, edit.new_string)
|
|
40
|
+
: content.replace(edit.old_string, edit.new_string);
|
|
41
|
+
}
|
|
42
|
+
await writeFile(file_path, content, "utf-8");
|
|
43
|
+
return `${edits.length} edit(s) applied successfully to ${file_path}`;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return `Error editing file: ${err.message}`;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (opts?.autoApprove === false) {
|
|
51
|
+
return Object.assign(baseTool, {
|
|
52
|
+
needsApproval: async () => true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return baseTool;
|
|
56
|
+
}
|
|
57
|
+
export const multiEditTool = createMultiEditTool();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
export const readTool = tool({
|
|
5
|
+
description: "Reads a file from the local filesystem. Returns content with line numbers (cat -n format).",
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
file_path: z.string().describe("Absolute path to the file to read"),
|
|
8
|
+
offset: z
|
|
9
|
+
.number()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("Line number to start reading from (1-based)"),
|
|
12
|
+
limit: z
|
|
13
|
+
.number()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Maximum number of lines to read"),
|
|
16
|
+
}),
|
|
17
|
+
execute: async ({ file_path, offset, limit }) => {
|
|
18
|
+
try {
|
|
19
|
+
const content = await readFile(file_path, "utf-8");
|
|
20
|
+
let lines = content.split("\n");
|
|
21
|
+
const start = offset ? offset - 1 : 0;
|
|
22
|
+
const end = limit ? start + limit : lines.length;
|
|
23
|
+
lines = lines.slice(start, end);
|
|
24
|
+
const maxLineNum = start + lines.length;
|
|
25
|
+
const pad = String(maxLineNum).length;
|
|
26
|
+
const numbered = lines.map((line, i) => `${String(start + i + 1).padStart(pad)}\t${line}`);
|
|
27
|
+
return numbered.join("\n");
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
return `Error reading file: ${err.message}`;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for spawning a sub-agent via the Task tool.
|
|
3
|
+
* Inherits model/apiKey from the parent agent.
|
|
4
|
+
*/
|
|
5
|
+
export interface TaskConfig {
|
|
6
|
+
model: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
/** Maximum steps the sub-agent can take (default: 10) */
|
|
9
|
+
maxSubSteps?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Factory that creates the Task tool for launching sub-agents.
|
|
13
|
+
*
|
|
14
|
+
* If no config is provided, returns a tool that explains it's not configured.
|
|
15
|
+
* When configured, uses runAiAgent() internally to spawn an isolated sub-agent
|
|
16
|
+
* that inherits the parent's model and apiKey but has no access to conversation history.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createTaskTool(config?: TaskConfig): import("ai").Tool<{
|
|
19
|
+
description: string;
|
|
20
|
+
prompt: string;
|
|
21
|
+
}, string>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const MAX_OUTPUT = 50_000;
|
|
4
|
+
/**
|
|
5
|
+
* Factory that creates the Task tool for launching sub-agents.
|
|
6
|
+
*
|
|
7
|
+
* If no config is provided, returns a tool that explains it's not configured.
|
|
8
|
+
* When configured, uses runAiAgent() internally to spawn an isolated sub-agent
|
|
9
|
+
* that inherits the parent's model and apiKey but has no access to conversation history.
|
|
10
|
+
*/
|
|
11
|
+
export function createTaskTool(config) {
|
|
12
|
+
return tool({
|
|
13
|
+
description: "Launch a sub-agent to handle a task autonomously. The sub-agent receives the same coding tools but runs in an isolated session without access to the parent's conversation history. Use this for tasks that can be completed independently.",
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
description: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe("Short description of the task (3-5 words)"),
|
|
18
|
+
prompt: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe("Detailed instructions for the sub-agent to execute"),
|
|
21
|
+
}),
|
|
22
|
+
execute: async ({ description, prompt }) => {
|
|
23
|
+
if (!config) {
|
|
24
|
+
return "Task tool not configured. The consuming application must provide model and apiKey configuration to enable sub-agent spawning.";
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
// Dynamic import to avoid circular dependency at module load time
|
|
28
|
+
const { runAiAgent } = await import("../agent.js");
|
|
29
|
+
let resultText = "";
|
|
30
|
+
const generator = runAiAgent(prompt, {
|
|
31
|
+
model: config.model,
|
|
32
|
+
apiKey: config.apiKey,
|
|
33
|
+
maxSteps: config.maxSubSteps ?? 10,
|
|
34
|
+
});
|
|
35
|
+
for await (const event of generator) {
|
|
36
|
+
if (event.type === "result") {
|
|
37
|
+
resultText = event.content;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let output = `## Sub-agent result for: "${description}"\n\n${resultText}`;
|
|
41
|
+
if (output.length > MAX_OUTPUT) {
|
|
42
|
+
output = output.slice(0, MAX_OUTPUT) + "\n...[truncated at 50KB]";
|
|
43
|
+
}
|
|
44
|
+
return output;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return `Error running sub-agent "${description}": ${err.message}`;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AiTodoItem } from "../types.js";
|
|
2
|
+
/** Reset state — useful for testing or when starting a new session */
|
|
3
|
+
export declare function resetTodoState(): void;
|
|
4
|
+
/** Read current todos — used internally by TodoRead and available for consumers */
|
|
5
|
+
export declare function getTodos(): AiTodoItem[];
|
|
6
|
+
export declare const todoWriteTool: import("ai").Tool<{
|
|
7
|
+
todos: {
|
|
8
|
+
status: "pending" | "in_progress" | "completed";
|
|
9
|
+
id: string;
|
|
10
|
+
content: string;
|
|
11
|
+
priority: "high" | "medium" | "low";
|
|
12
|
+
}[];
|
|
13
|
+
}, string>;
|
|
14
|
+
export declare const todoReadTool: import("ai").Tool<{}, string>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// Module-level state: session todos keyed by a simple singleton
|
|
4
|
+
// (one agent run = one process = one list)
|
|
5
|
+
let currentTodos = [];
|
|
6
|
+
/** Reset state — useful for testing or when starting a new session */
|
|
7
|
+
export function resetTodoState() {
|
|
8
|
+
currentTodos = [];
|
|
9
|
+
}
|
|
10
|
+
/** Read current todos — used internally by TodoRead and available for consumers */
|
|
11
|
+
export function getTodos() {
|
|
12
|
+
return [...currentTodos];
|
|
13
|
+
}
|
|
14
|
+
const todoItemSchema = z.object({
|
|
15
|
+
id: z.string().describe("Unique identifier for the todo item"),
|
|
16
|
+
content: z.string().describe("Description of the task"),
|
|
17
|
+
status: z
|
|
18
|
+
.enum(["pending", "in_progress", "completed"])
|
|
19
|
+
.describe("Current status of the task"),
|
|
20
|
+
priority: z
|
|
21
|
+
.enum(["high", "medium", "low"])
|
|
22
|
+
.describe("Priority level of the task"),
|
|
23
|
+
});
|
|
24
|
+
export const todoWriteTool = tool({
|
|
25
|
+
description: "Creates or updates the task list for the current session. Replaces the entire list with the provided todos. Use this to track progress on multi-step tasks.",
|
|
26
|
+
inputSchema: z.object({
|
|
27
|
+
todos: z
|
|
28
|
+
.array(todoItemSchema)
|
|
29
|
+
.min(1)
|
|
30
|
+
.describe("The complete list of todos (replaces any existing list)"),
|
|
31
|
+
}),
|
|
32
|
+
execute: async ({ todos }) => {
|
|
33
|
+
currentTodos = todos;
|
|
34
|
+
const pending = currentTodos.filter((t) => t.status === "pending").length;
|
|
35
|
+
const inProgress = currentTodos.filter((t) => t.status === "in_progress").length;
|
|
36
|
+
const completed = currentTodos.filter((t) => t.status === "completed").length;
|
|
37
|
+
return `Todo list updated: ${currentTodos.length} items (${completed} completed, ${inProgress} in progress, ${pending} pending)`;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
export const todoReadTool = tool({
|
|
41
|
+
description: "Reads the current task list for the session. Returns all todos with their status and priority.",
|
|
42
|
+
inputSchema: z.object({}),
|
|
43
|
+
execute: async () => {
|
|
44
|
+
if (currentTodos.length === 0) {
|
|
45
|
+
return "Nenhuma tarefa registrada.";
|
|
46
|
+
}
|
|
47
|
+
const statusIcon = {
|
|
48
|
+
pending: "○",
|
|
49
|
+
in_progress: "◐",
|
|
50
|
+
completed: "●",
|
|
51
|
+
};
|
|
52
|
+
const priorityTag = {
|
|
53
|
+
high: "[HIGH]",
|
|
54
|
+
medium: "[MED]",
|
|
55
|
+
low: "[LOW]",
|
|
56
|
+
};
|
|
57
|
+
const lines = currentTodos.map((t) => `${statusIcon[t.status]} ${priorityTag[t.priority]} ${t.id}: ${t.content} (${t.status})`);
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const MAX_OUTPUT = 50_000;
|
|
4
|
+
const FETCH_TIMEOUT = 30_000;
|
|
5
|
+
/**
|
|
6
|
+
* Converts HTML to a simplified markdown representation.
|
|
7
|
+
* Handles common elements: headings, links, lists, paragraphs, code blocks, bold, italic.
|
|
8
|
+
*/
|
|
9
|
+
function htmlToMarkdown(html) {
|
|
10
|
+
let text = html;
|
|
11
|
+
// Remove script, style, nav, footer, header tags and their content
|
|
12
|
+
text = text.replace(/<(script|style|nav|footer|header|aside|svg|noscript)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
13
|
+
// Remove HTML comments
|
|
14
|
+
text = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
15
|
+
// Headings
|
|
16
|
+
text = text.replace(/<h1\b[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n");
|
|
17
|
+
text = text.replace(/<h2\b[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n");
|
|
18
|
+
text = text.replace(/<h3\b[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n");
|
|
19
|
+
text = text.replace(/<h4\b[^>]*>([\s\S]*?)<\/h4>/gi, "\n#### $1\n");
|
|
20
|
+
text = text.replace(/<h5\b[^>]*>([\s\S]*?)<\/h5>/gi, "\n##### $1\n");
|
|
21
|
+
text = text.replace(/<h6\b[^>]*>([\s\S]*?)<\/h6>/gi, "\n###### $1\n");
|
|
22
|
+
// Code blocks (pre > code)
|
|
23
|
+
text = text.replace(/<pre\b[^>]*>\s*<code\b[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, "\n```\n$1\n```\n");
|
|
24
|
+
text = text.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, "\n```\n$1\n```\n");
|
|
25
|
+
// Inline code
|
|
26
|
+
text = text.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, "`$1`");
|
|
27
|
+
// Bold
|
|
28
|
+
text = text.replace(/<(strong|b)\b[^>]*>([\s\S]*?)<\/\1>/gi, "**$2**");
|
|
29
|
+
// Italic
|
|
30
|
+
text = text.replace(/<(em|i)\b[^>]*>([\s\S]*?)<\/\1>/gi, "*$2*");
|
|
31
|
+
// Links
|
|
32
|
+
text = text.replace(/<a\b[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)");
|
|
33
|
+
// Images
|
|
34
|
+
text = text.replace(/<img\b[^>]*alt="([^"]*)"[^>]*src="([^"]*)"[^>]*\/?>/gi, "");
|
|
35
|
+
text = text.replace(/<img\b[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, "");
|
|
36
|
+
text = text.replace(/<img\b[^>]*src="([^"]*)"[^>]*\/?>/gi, "");
|
|
37
|
+
// List items
|
|
38
|
+
text = text.replace(/<li\b[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n");
|
|
39
|
+
// Unordered/ordered list wrappers
|
|
40
|
+
text = text.replace(/<\/?(ul|ol)\b[^>]*>/gi, "\n");
|
|
41
|
+
// Paragraphs and divs
|
|
42
|
+
text = text.replace(/<\/?p\b[^>]*>/gi, "\n");
|
|
43
|
+
text = text.replace(/<\/?div\b[^>]*>/gi, "\n");
|
|
44
|
+
// Line breaks
|
|
45
|
+
text = text.replace(/<br\s*\/?>/gi, "\n");
|
|
46
|
+
// Horizontal rules
|
|
47
|
+
text = text.replace(/<hr\b[^>]*\/?>/gi, "\n---\n");
|
|
48
|
+
// Table handling (basic)
|
|
49
|
+
text = text.replace(/<\/tr>/gi, "\n");
|
|
50
|
+
text = text.replace(/<\/?(table|thead|tbody|tfoot)\b[^>]*>/gi, "\n");
|
|
51
|
+
text = text.replace(/<th\b[^>]*>([\s\S]*?)<\/th>/gi, "| **$1** ");
|
|
52
|
+
text = text.replace(/<td\b[^>]*>([\s\S]*?)<\/td>/gi, "| $1 ");
|
|
53
|
+
// Strip remaining HTML tags
|
|
54
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
55
|
+
// Decode common HTML entities
|
|
56
|
+
text = text.replace(/&/g, "&");
|
|
57
|
+
text = text.replace(/</g, "<");
|
|
58
|
+
text = text.replace(/>/g, ">");
|
|
59
|
+
text = text.replace(/"/g, '"');
|
|
60
|
+
text = text.replace(/'/g, "'");
|
|
61
|
+
text = text.replace(/ /g, " ");
|
|
62
|
+
text = text.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)));
|
|
63
|
+
// Clean up whitespace: collapse multiple blank lines
|
|
64
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
65
|
+
text = text.trim();
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
68
|
+
export const webFetchTool = tool({
|
|
69
|
+
description: "Fetches content from a URL and returns it as markdown/text. Converts HTML to markdown. Timeout: 30s. Max output: 50KB.",
|
|
70
|
+
inputSchema: z.object({
|
|
71
|
+
url: z.string().url().describe("The URL to fetch content from"),
|
|
72
|
+
prompt: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Extraction instruction — what to look for in the content"),
|
|
76
|
+
}),
|
|
77
|
+
execute: async ({ url, prompt }) => {
|
|
78
|
+
try {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
headers: {
|
|
84
|
+
"User-Agent": "AiSDK/1.0 (WebFetch tool)",
|
|
85
|
+
Accept: "text/html, application/json, text/plain, */*",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
return `Error: HTTP ${response.status} ${response.statusText}`;
|
|
91
|
+
}
|
|
92
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
93
|
+
const rawBody = await response.text();
|
|
94
|
+
let content;
|
|
95
|
+
if (contentType.includes("text/html")) {
|
|
96
|
+
content = htmlToMarkdown(rawBody);
|
|
97
|
+
}
|
|
98
|
+
else if (contentType.includes("application/json")) {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(rawBody);
|
|
101
|
+
content = JSON.stringify(parsed, null, 2);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
content = rawBody;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
content = rawBody;
|
|
109
|
+
}
|
|
110
|
+
if (content.length > MAX_OUTPUT) {
|
|
111
|
+
content = content.slice(0, MAX_OUTPUT) + "\n...[truncated at 50KB]";
|
|
112
|
+
}
|
|
113
|
+
let result = `# Fetched: ${url}\n\n${content}`;
|
|
114
|
+
if (prompt) {
|
|
115
|
+
result = `# Fetched: ${url}\n# Extraction: ${prompt}\n\n${content}`;
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (err.name === "AbortError") {
|
|
121
|
+
return `Error: Request timed out after ${FETCH_TIMEOUT / 1000}s`;
|
|
122
|
+
}
|
|
123
|
+
return `Error fetching URL: ${err.message}`;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single search result returned by a WebSearch provider.
|
|
3
|
+
*/
|
|
4
|
+
export interface WebSearchResult {
|
|
5
|
+
title: string;
|
|
6
|
+
url: string;
|
|
7
|
+
snippet: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Callback type for pluggable web search providers.
|
|
11
|
+
* Implementors receive the query and desired number of results,
|
|
12
|
+
* and return an array of WebSearchResult.
|
|
13
|
+
*/
|
|
14
|
+
export type WebSearchProvider = (query: string, numResults: number) => Promise<WebSearchResult[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Factory that creates the WebSearch tool with an injected search provider.
|
|
17
|
+
* If no provider is given, returns a tool that explains no provider is configured.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createWebSearchTool(searchProvider?: WebSearchProvider): import("ai").Tool<{
|
|
20
|
+
query: string;
|
|
21
|
+
numResults: number;
|
|
22
|
+
}, string>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const MAX_OUTPUT = 50_000;
|
|
4
|
+
/**
|
|
5
|
+
* Factory that creates the WebSearch tool with an injected search provider.
|
|
6
|
+
* If no provider is given, returns a tool that explains no provider is configured.
|
|
7
|
+
*/
|
|
8
|
+
export function createWebSearchTool(searchProvider) {
|
|
9
|
+
return tool({
|
|
10
|
+
description: "Search the web and return formatted results with title, URL, and snippet. Use this to find up-to-date information, documentation, or answers to questions.",
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
query: z.string().describe("The search query"),
|
|
13
|
+
numResults: z
|
|
14
|
+
.number()
|
|
15
|
+
.int()
|
|
16
|
+
.min(1)
|
|
17
|
+
.max(20)
|
|
18
|
+
.optional()
|
|
19
|
+
.default(5)
|
|
20
|
+
.describe("Number of results to return (default: 5)"),
|
|
21
|
+
}),
|
|
22
|
+
execute: async ({ query, numResults }) => {
|
|
23
|
+
if (!searchProvider) {
|
|
24
|
+
return "WebSearch provider not configured. The consuming application must provide an onWebSearch callback in AiAgentOptions to enable web search.";
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const results = await searchProvider(query, numResults);
|
|
28
|
+
if (results.length === 0) {
|
|
29
|
+
return `No results found for: "${query}"`;
|
|
30
|
+
}
|
|
31
|
+
let output = `# Search results for: "${query}"\n\n`;
|
|
32
|
+
for (let i = 0; i < results.length; i++) {
|
|
33
|
+
const r = results[i];
|
|
34
|
+
output += `## ${i + 1}. ${r.title}\n`;
|
|
35
|
+
output += `**URL:** ${r.url}\n`;
|
|
36
|
+
output += `${r.snippet}\n\n`;
|
|
37
|
+
}
|
|
38
|
+
if (output.length > MAX_OUTPUT) {
|
|
39
|
+
output = output.slice(0, MAX_OUTPUT) + "\n...[truncated at 50KB]";
|
|
40
|
+
}
|
|
41
|
+
return output.trim();
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return `Error searching web: ${err.message}`;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|