@vietor/agent-core 0.7.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/LICENSE +21 -0
- package/README.md +849 -0
- package/dist/create-session.d.ts +4 -0
- package/dist/create-session.js +51 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +5 -0
- package/dist/llm/anthropic.d.ts +14 -0
- package/dist/llm/anthropic.js +200 -0
- package/dist/llm/base.d.ts +10 -0
- package/dist/llm/base.js +12 -0
- package/dist/llm/client.d.ts +4 -0
- package/dist/llm/client.js +61 -0
- package/dist/llm/completions.d.ts +8 -0
- package/dist/llm/completions.js +88 -0
- package/dist/llm/messages.d.ts +50 -0
- package/dist/llm/messages.js +28 -0
- package/dist/llm/responses.d.ts +14 -0
- package/dist/llm/responses.js +130 -0
- package/dist/llm/types.d.ts +40 -0
- package/dist/llm/types.js +1 -0
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +53 -0
- package/dist/mcp/manager.d.ts +18 -0
- package/dist/mcp/manager.js +155 -0
- package/dist/mcp/types.d.ts +26 -0
- package/dist/mcp/types.js +1 -0
- package/dist/runtime/agent.d.ts +56 -0
- package/dist/runtime/agent.js +253 -0
- package/dist/runtime/events.d.ts +69 -0
- package/dist/runtime/events.js +1 -0
- package/dist/runtime/prompts.d.ts +5 -0
- package/dist/runtime/prompts.js +45 -0
- package/dist/runtime/session-messages.d.ts +43 -0
- package/dist/runtime/session-messages.js +174 -0
- package/dist/runtime/session.d.ts +102 -0
- package/dist/runtime/session.js +375 -0
- package/dist/runtime/sub-agent-runner.d.ts +18 -0
- package/dist/runtime/sub-agent-runner.js +26 -0
- package/dist/runtime/timeline.d.ts +21 -0
- package/dist/runtime/timeline.js +146 -0
- package/dist/runtime/todo-store.d.ts +8 -0
- package/dist/runtime/todo-store.js +15 -0
- package/dist/skills/loader.d.ts +6 -0
- package/dist/skills/loader.js +43 -0
- package/dist/tools/ask-user.d.ts +3 -0
- package/dist/tools/ask-user.js +26 -0
- package/dist/tools/file-edit.d.ts +2 -0
- package/dist/tools/file-edit.js +43 -0
- package/dist/tools/file-read.d.ts +2 -0
- package/dist/tools/file-read.js +93 -0
- package/dist/tools/file-write.d.ts +2 -0
- package/dist/tools/file-write.js +25 -0
- package/dist/tools/glob.d.ts +2 -0
- package/dist/tools/glob.js +31 -0
- package/dist/tools/grep.d.ts +2 -0
- package/dist/tools/grep.js +67 -0
- package/dist/tools/registry.d.ts +30 -0
- package/dist/tools/registry.js +107 -0
- package/dist/tools/shell.d.ts +2 -0
- package/dist/tools/shell.js +57 -0
- package/dist/tools/skill.d.ts +3 -0
- package/dist/tools/skill.js +30 -0
- package/dist/tools/sub-agent.d.ts +7 -0
- package/dist/tools/sub-agent.js +81 -0
- package/dist/tools/todo-write.d.ts +3 -0
- package/dist/tools/todo-write.js +72 -0
- package/dist/tools/types.d.ts +32 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/web-fetch.d.ts +2 -0
- package/dist/tools/web-fetch.js +104 -0
- package/dist/util/async.d.ts +19 -0
- package/dist/util/async.js +93 -0
- package/dist/util/constants.d.ts +25 -0
- package/dist/util/constants.js +27 -0
- package/dist/util/emitter.d.ts +5 -0
- package/dist/util/emitter.js +15 -0
- package/dist/util/file.d.ts +3 -0
- package/dist/util/file.js +19 -0
- package/dist/util/html.d.ts +1 -0
- package/dist/util/html.js +14 -0
- package/dist/util/index.d.ts +7 -0
- package/dist/util/index.js +7 -0
- package/dist/util/net.d.ts +1 -0
- package/dist/util/net.js +31 -0
- package/dist/util/ripgrep.d.ts +10 -0
- package/dist/util/ripgrep.js +34 -0
- package/dist/util/subprocess.d.ts +15 -0
- package/dist/util/subprocess.js +113 -0
- package/dist/util/text.d.ts +15 -0
- package/dist/util/text.js +72 -0
- package/package.json +52 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { NOT_EXECUTED_PREFIX } from "../util/constants.js";
|
|
2
|
+
import { toolError } from "./types.js";
|
|
3
|
+
export const SUB_AGENT_GUIDANCE = '- Consider delegating to the SubAgent tool when the task matches an agent type, when you have independent work to run in parallel, or when answering would mean reading across several files — delegate and keep the conclusion, not the file dumps. type: "explore" — read-only search agent for broad fan-out searches (state the search breadth in the task); type: "plan" — software architect producing implementation plans. For a single-fact lookup where you already know the file, symbol, or value, search directly. Once you have delegated a search, do not also run it yourself — wait for the result. Issue at most 2 SubAgent calls per turn; multiple calls in the same turn run concurrently. Sub-agents are read-only and return only their final report, not intermediate steps — verify important results yourself. For large workloads with many independent items that would exceed the turn budget, split the items into chunks sized so each sub-agent can complete its chunk within its own loop budget, delegate one SubAgent per chunk, and run the remaining chunks in the following turns as results return. Instruct each sub-agent to report results per item in structured lines so you can consolidate.';
|
|
4
|
+
const EXPLORE_PROMPT = [
|
|
5
|
+
"You are the Explore sub-agent — a read-only search agent for broad fan-out searches. Use it when answering means sweeping many files, directories, or naming conventions and the parent needs only the conclusion, not the file dumps. You read excerpts rather than whole files, so you locate code — you do not review or audit it. You are read-only: you must not modify any files.",
|
|
6
|
+
"Guidelines:",
|
|
7
|
+
'- If the parent stated a search breadth, match your effort to it: "medium" for moderate exploration, "very thorough" for multiple locations and naming conventions.',
|
|
8
|
+
"- Use Grep and Glob to locate matches first, then read only the excerpts needed to extract the facts — not whole files.",
|
|
9
|
+
"- Follow imports and call sites to trace definitions when the answer depends on how code connects.",
|
|
10
|
+
"- Trust tool results as ground truth; do not guess file contents from memory.",
|
|
11
|
+
"- If the task is ambiguous, state your assumptions explicitly.",
|
|
12
|
+
'- Report in concise markdown: a summary of findings first, then details with file_path:line_number references, and a final "Bottom line" section with a direct answer to the task.',
|
|
13
|
+
"- Keep the reply proportionate to the question — typically 10-40 lines; extract key facts rather than pasting file contents.",
|
|
14
|
+
].join("\n");
|
|
15
|
+
const PLAN_PROMPT = [
|
|
16
|
+
"You are the Plan sub-agent — a software architect. Produce a step-by-step implementation plan for the given task. Read the relevant code first to ground the plan in the actual code, then design the plan. You are read-only: you must not modify any files or implement anything.",
|
|
17
|
+
"Guidelines:",
|
|
18
|
+
"- First locate the relevant code: read the files the task mentions and confirm real function signatures, module structure, and existing conventions before planning.",
|
|
19
|
+
"- Consider architectural trade-offs: note the alternative approaches and why the recommended one was chosen.",
|
|
20
|
+
"- Output a numbered step-by-step plan in markdown. For each step give the file paths to create or modify, the function or type signatures involved, and a one-line rationale. Order steps by dependency.",
|
|
21
|
+
"- Identify the critical files for implementation — the files the implementer must read first.",
|
|
22
|
+
'- End with a short "Risks & open questions" section listing anything to verify during implementation.',
|
|
23
|
+
'- End with a short "Verification" section: the commands, tests, or manual checks to run to confirm each step works.',
|
|
24
|
+
"- Keep the plan concise — typically 20-50 lines.",
|
|
25
|
+
"- Be specific and actionable; do not speculate beyond what you read.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
const SUB_AGENT_DEFS = [
|
|
28
|
+
{
|
|
29
|
+
type: "explore",
|
|
30
|
+
name: "Explore",
|
|
31
|
+
description: "Read-only search agent for broad fan-out searches across the codebase or web; locate code via excerpts, does not review or audit.",
|
|
32
|
+
systemPrompt: EXPLORE_PROMPT,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: "plan",
|
|
36
|
+
name: "Plan",
|
|
37
|
+
description: "Software architect that produces a step-by-step implementation plan grounded in the actual code.",
|
|
38
|
+
systemPrompt: PLAN_PROMPT,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
const TOOL_DESCRIPTION = 'Run a dedicated sub-agent in its own nested loop — the only result you receive is its final report as text, not intermediate steps. type: "explore" — a read-only search agent for broad fan-out searches of the codebase or web; specify a search breadth in the task ("medium" for moderate exploration, "very thorough" for multiple locations and naming conventions). type: "plan" — a software architect that reads the relevant code first, then returns a step-by-step implementation plan identifying critical files and architectural trade-offs. Sub-agents are read-only and cannot ask questions, use skills, or spawn further sub-agents.';
|
|
42
|
+
export function createSubAgentTool(deps) {
|
|
43
|
+
return {
|
|
44
|
+
name: "SubAgent",
|
|
45
|
+
description: TOOL_DESCRIPTION,
|
|
46
|
+
parameters: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
type: {
|
|
50
|
+
type: "string",
|
|
51
|
+
enum: SUB_AGENT_DEFS.map((d) => d.type),
|
|
52
|
+
description: 'The sub-agent type to invoke: "explore" (read-only fan-out search) or "plan" (implementation plan).',
|
|
53
|
+
},
|
|
54
|
+
task: { type: "string", description: "The task or question for the sub-agent, as a self-contained description." },
|
|
55
|
+
},
|
|
56
|
+
required: ["type", "task"],
|
|
57
|
+
},
|
|
58
|
+
summaryKeys: ["type"],
|
|
59
|
+
async execute(args, ctx) {
|
|
60
|
+
const type = args.type;
|
|
61
|
+
const task = (args.task ?? "").trim();
|
|
62
|
+
const def = SUB_AGENT_DEFS.find((d) => d.type === type);
|
|
63
|
+
if (!def) {
|
|
64
|
+
return toolError(`unknown sub-agent type "${type}". Valid types: ${SUB_AGENT_DEFS.map((d) => d.type).join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
if (!task) {
|
|
67
|
+
return toolError("task is required");
|
|
68
|
+
}
|
|
69
|
+
const { status, reply, messages } = await deps.runSubAgent(def.systemPrompt, task, ctx.signal);
|
|
70
|
+
if (status === "ok")
|
|
71
|
+
return { content: reply };
|
|
72
|
+
const stallReason = status === "stalled"
|
|
73
|
+
? [...messages].reverse()
|
|
74
|
+
.map((m) => m.content)
|
|
75
|
+
.find((c) => typeof c === "string" && c.startsWith(NOT_EXECUTED_PREFIX))
|
|
76
|
+
: undefined;
|
|
77
|
+
const suffix = stallReason ? ` ${stallReason}` : "";
|
|
78
|
+
return { content: `Sub-agent "${type}" ended with status ${status}.${suffix}\n\n${reply}`, isError: true };
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Tool, Todo } from "./types.js";
|
|
2
|
+
export declare const TODO_WRITE_GUIDANCE = "- For multi-step tasks (3+ steps), you MUST use TodoWrite: create the task list first, then update each task's status as you execute. Never execute a multi-step task without a TodoWrite task list.";
|
|
3
|
+
export declare function createTodoWriteTool(setTodos: (todos: Todo[]) => void): Tool;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { toolError } from "./types.js";
|
|
2
|
+
export const TODO_WRITE_GUIDANCE = "- For multi-step tasks (3+ steps), you MUST use TodoWrite: create the task list first, then update each task's status as you execute. Never execute a multi-step task without a TodoWrite task list.";
|
|
3
|
+
const STATUSES = ["pending", "inProgress", "completed"];
|
|
4
|
+
const DESCRIPTION = "Manage the task list for tasks with 3+ steps. Pass the FULL list each call; it replaces the previous list. Keep one inProgress at a time. status: pending, inProgress, completed.";
|
|
5
|
+
function parseTodos(args) {
|
|
6
|
+
const raw = args.todos;
|
|
7
|
+
if (!Array.isArray(raw)) {
|
|
8
|
+
return { todos: [], done: 0, normalized: 0, error: `"todos" must be an array of {content, status} objects, got ${typeof raw}` };
|
|
9
|
+
}
|
|
10
|
+
const todos = [];
|
|
11
|
+
let done = 0;
|
|
12
|
+
let normalized = 0;
|
|
13
|
+
let seenInProgress = false;
|
|
14
|
+
for (const t of raw) {
|
|
15
|
+
if (!t)
|
|
16
|
+
continue;
|
|
17
|
+
const content = typeof t.content === "string" ? t.content.trim() : "";
|
|
18
|
+
if (!content)
|
|
19
|
+
continue;
|
|
20
|
+
let status = STATUSES.includes(t.status) ? t.status : "pending";
|
|
21
|
+
if (status === "inProgress") {
|
|
22
|
+
if (seenInProgress) {
|
|
23
|
+
status = "pending";
|
|
24
|
+
normalized++;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
seenInProgress = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (status === "completed") {
|
|
31
|
+
done++;
|
|
32
|
+
}
|
|
33
|
+
todos.push({ content, status });
|
|
34
|
+
}
|
|
35
|
+
return { todos, done, normalized };
|
|
36
|
+
}
|
|
37
|
+
export function createTodoWriteTool(setTodos) {
|
|
38
|
+
return {
|
|
39
|
+
name: "TodoWrite",
|
|
40
|
+
description: DESCRIPTION,
|
|
41
|
+
parameters: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
todos: {
|
|
45
|
+
type: "array",
|
|
46
|
+
description: "The full task list, in execution order.",
|
|
47
|
+
items: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
content: { type: "string", description: "A short imperative description of the step." },
|
|
51
|
+
status: { type: "string", enum: STATUSES, description: "Current status of the step." },
|
|
52
|
+
},
|
|
53
|
+
required: ["content", "status"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required: ["todos"],
|
|
58
|
+
},
|
|
59
|
+
summarizeArgs(args) {
|
|
60
|
+
const { todos, done, error } = parseTodos(args);
|
|
61
|
+
return error ? "invalid" : `${done}/${todos.length}`;
|
|
62
|
+
},
|
|
63
|
+
async execute(args, _ctx) {
|
|
64
|
+
const { todos, done, normalized, error } = parseTodos(args);
|
|
65
|
+
if (error)
|
|
66
|
+
return toolError(error);
|
|
67
|
+
setTodos(todos);
|
|
68
|
+
const note = normalized ? ` (normalized ${normalized} item${normalized === 1 ? "" : "s"} to one inProgress)` : "";
|
|
69
|
+
return { content: `Updated task list (${todos.length} item${todos.length === 1 ? "" : "s"}, ${done} done)${note}.` };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface TextResult {
|
|
2
|
+
content: string;
|
|
3
|
+
isError?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function toolError(msg: string): TextResult;
|
|
6
|
+
export type TodoStatus = "pending" | "inProgress" | "completed";
|
|
7
|
+
export interface Todo {
|
|
8
|
+
content: string;
|
|
9
|
+
status: TodoStatus;
|
|
10
|
+
}
|
|
11
|
+
export interface ToolContext {
|
|
12
|
+
cwd: string;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
export interface ToolSchema {
|
|
16
|
+
type: "function";
|
|
17
|
+
function: {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
parameters: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface Tool {
|
|
24
|
+
name: string;
|
|
25
|
+
readOnly?: boolean;
|
|
26
|
+
description: string;
|
|
27
|
+
parameters: Record<string, unknown>;
|
|
28
|
+
summaryKeys?: string[];
|
|
29
|
+
summarizeArgs?: (args: Record<string, unknown>) => string;
|
|
30
|
+
summarizeResult?(result: TextResult): string;
|
|
31
|
+
execute(args: Record<string, unknown>, ctx: ToolContext): Promise<TextResult>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { netFetch } from "../util/net.js";
|
|
2
|
+
import { backoffDelay, isAbortError, withRetry, withTimeoutFn } from "../util/async.js";
|
|
3
|
+
import { MAX_WEB_FETCH_MB, mbToBytes, REQUEST_TIMEOUT_MS, WEB_FETCH_RETRIES } from "../util/constants.js";
|
|
4
|
+
import { htmlToMarkdown } from "../util/html.js";
|
|
5
|
+
import { toErrorMessage, summaryBytes } from "../util/text.js";
|
|
6
|
+
function mimeFrom(contentType) {
|
|
7
|
+
return contentType.split(";", 1)[0].trim().toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
function isTextualMime(mime) {
|
|
10
|
+
return (!mime ||
|
|
11
|
+
mime.startsWith("text/") ||
|
|
12
|
+
mime === "application/json" ||
|
|
13
|
+
mime.endsWith("+json") ||
|
|
14
|
+
mime === "application/xml" ||
|
|
15
|
+
mime.endsWith("+xml") ||
|
|
16
|
+
mime === "application/javascript" ||
|
|
17
|
+
mime === "application/x-javascript");
|
|
18
|
+
}
|
|
19
|
+
const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
20
|
+
const MAX_WEB_FETCH_BYTES = mbToBytes(MAX_WEB_FETCH_MB);
|
|
21
|
+
const REQUEST_HEADERS = {
|
|
22
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
|
|
23
|
+
"Accept": "text/markdown,text/html,text/plain,application/xhtml+xml,application/xml,application/json;q=0.9,image/webp,*/*;q=0.8",
|
|
24
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
25
|
+
"Cache-Control": "no-cache",
|
|
26
|
+
};
|
|
27
|
+
const DESCRIPTION = `Fetch a URL via HTTP GET. Returns raw text for JSON/XML/text; converts HTML to markdown. Rejects binary content and bodies over ${MAX_WEB_FETCH_MB}MB. Retries transient failures (network, timeouts, 429/5xx) up to ${WEB_FETCH_RETRIES + 1} attempts. GET only; no custom headers or request body. Follows redirects.`;
|
|
28
|
+
class WebFetchError extends Error {
|
|
29
|
+
}
|
|
30
|
+
async function readTextBounded(res, maxBytes) {
|
|
31
|
+
if (!res.body)
|
|
32
|
+
return "";
|
|
33
|
+
const reader = res.body.getReader();
|
|
34
|
+
const decoder = new TextDecoder();
|
|
35
|
+
let text = "";
|
|
36
|
+
let bytes = 0;
|
|
37
|
+
for (;;) {
|
|
38
|
+
const { done, value } = await reader.read();
|
|
39
|
+
if (done)
|
|
40
|
+
break;
|
|
41
|
+
bytes += value.byteLength;
|
|
42
|
+
if (bytes > maxBytes) {
|
|
43
|
+
await reader.cancel().catch(() => { });
|
|
44
|
+
throw new Error(`response body exceeds ${maxBytes} bytes`);
|
|
45
|
+
}
|
|
46
|
+
text += decoder.decode(value, { stream: true });
|
|
47
|
+
}
|
|
48
|
+
return text + decoder.decode();
|
|
49
|
+
}
|
|
50
|
+
async function fetchOne(url, signal) {
|
|
51
|
+
const res = await withTimeoutFn((s) => netFetch(url, { headers: REQUEST_HEADERS, redirect: "follow", signal: s }), REQUEST_TIMEOUT_MS, signal, `timed out after ${REQUEST_TIMEOUT_MS / 1000}s`).catch((e) => {
|
|
52
|
+
if (isAbortError(e))
|
|
53
|
+
throw e;
|
|
54
|
+
throw new WebFetchError(`failed to fetch ${url}: ${toErrorMessage(e)}`);
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const message = `${res.status} ${res.statusText} for ${url}`;
|
|
58
|
+
if (RETRYABLE_STATUS.has(res.status))
|
|
59
|
+
throw new WebFetchError(message);
|
|
60
|
+
throw new Error(message);
|
|
61
|
+
}
|
|
62
|
+
const contentType = res.headers.get("content-type") || "";
|
|
63
|
+
const mime = mimeFrom(contentType);
|
|
64
|
+
if (!isTextualMime(mime))
|
|
65
|
+
throw new Error(`unsupported content type: ${mime} for ${url}`);
|
|
66
|
+
const contentLength = Number(res.headers.get("content-length"));
|
|
67
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_WEB_FETCH_BYTES) {
|
|
68
|
+
throw new Error(`content too large: ${contentLength} bytes for ${url} (limit ${MAX_WEB_FETCH_BYTES})`);
|
|
69
|
+
}
|
|
70
|
+
const body = await readTextBounded(res, MAX_WEB_FETCH_BYTES);
|
|
71
|
+
if (!mime.includes("html"))
|
|
72
|
+
return body;
|
|
73
|
+
return htmlToMarkdown(body);
|
|
74
|
+
}
|
|
75
|
+
export const webFetchTool = {
|
|
76
|
+
name: "WebFetch",
|
|
77
|
+
readOnly: true,
|
|
78
|
+
description: DESCRIPTION,
|
|
79
|
+
parameters: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
url: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "full URL including scheme (http or https)",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
required: ["url"],
|
|
88
|
+
},
|
|
89
|
+
async execute(args, ctx) {
|
|
90
|
+
const url = args.url;
|
|
91
|
+
return {
|
|
92
|
+
content: await withRetry(() => fetchOne(url, ctx.signal), {
|
|
93
|
+
retries: WEB_FETCH_RETRIES,
|
|
94
|
+
retryable: (e) => e instanceof WebFetchError,
|
|
95
|
+
backoff: backoffDelay,
|
|
96
|
+
signal: ctx.signal,
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
summarizeResult(result) {
|
|
101
|
+
return summaryBytes("Fetched", result, "Fetch failed");
|
|
102
|
+
},
|
|
103
|
+
summaryKeys: ["url"],
|
|
104
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare class AbortedError extends Error {
|
|
2
|
+
constructor();
|
|
3
|
+
}
|
|
4
|
+
export declare function isAbortError(e: unknown): boolean;
|
|
5
|
+
export declare function backoffDelay(attempt: number): number;
|
|
6
|
+
export interface RetryOptions {
|
|
7
|
+
retries: number;
|
|
8
|
+
retryable: (e: unknown) => boolean;
|
|
9
|
+
backoff: (attempt: number) => number;
|
|
10
|
+
onRetry?: (attempt: number, max: number, error: unknown) => void;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
export declare function withRetry<T>(fn: () => Promise<T>, opts: RetryOptions): Promise<T>;
|
|
14
|
+
export declare function withTimeout<T>(p: Promise<T>, ms: number): Promise<T>;
|
|
15
|
+
export declare function withTimeoutSignal(signal: AbortSignal | undefined, ms: number): AbortSignal;
|
|
16
|
+
export declare function isTimeout(signal: AbortSignal): boolean;
|
|
17
|
+
export declare function withAbort<T>(promise: Promise<T>, signal?: AbortSignal, onAbort?: () => T): Promise<T>;
|
|
18
|
+
export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T) => Promise<R>, signal?: AbortSignal): Promise<R[]>;
|
|
19
|
+
export declare function withTimeoutFn<T>(fn: (signal: AbortSignal) => Promise<T>, ms: number, signal: AbortSignal | undefined, timeoutMessage: string, otherError?: (e: unknown) => unknown): Promise<T>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export class AbortedError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("aborted");
|
|
4
|
+
this.name = "AbortedError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function isAbortError(e) {
|
|
8
|
+
if (e instanceof AbortedError)
|
|
9
|
+
return true;
|
|
10
|
+
const name = e?.name;
|
|
11
|
+
return name === "AbortError" || name === "APIUserAbortError";
|
|
12
|
+
}
|
|
13
|
+
export function backoffDelay(attempt) {
|
|
14
|
+
return 1000 * 2 ** attempt;
|
|
15
|
+
}
|
|
16
|
+
export async function withRetry(fn, opts) {
|
|
17
|
+
for (let attempt = 0;; attempt++) {
|
|
18
|
+
try {
|
|
19
|
+
return await fn();
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (attempt < opts.retries && opts.retryable(e)) {
|
|
23
|
+
opts.onRetry?.(attempt + 1, opts.retries, e);
|
|
24
|
+
await trySleep(opts.backoff(attempt), opts.signal);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function trySleep(ms, signal) {
|
|
32
|
+
if (!signal)
|
|
33
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
if (signal.aborted) {
|
|
36
|
+
reject(new AbortedError());
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const onAbort = () => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
reject(new AbortedError());
|
|
42
|
+
};
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
signal.removeEventListener("abort", onAbort);
|
|
45
|
+
resolve();
|
|
46
|
+
}, ms);
|
|
47
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function withTimeout(p, ms) {
|
|
51
|
+
return withTimeoutFn(() => p, ms, undefined, `timeout after ${ms}ms`);
|
|
52
|
+
}
|
|
53
|
+
export function withTimeoutSignal(signal, ms) {
|
|
54
|
+
return signal ? AbortSignal.any([signal, AbortSignal.timeout(ms)]) : AbortSignal.timeout(ms);
|
|
55
|
+
}
|
|
56
|
+
export function isTimeout(signal) {
|
|
57
|
+
return signal.reason?.name === "TimeoutError";
|
|
58
|
+
}
|
|
59
|
+
export function withAbort(promise, signal, onAbort) {
|
|
60
|
+
if (!signal)
|
|
61
|
+
return promise;
|
|
62
|
+
if (signal.aborted)
|
|
63
|
+
return onAbort ? Promise.resolve(onAbort()) : Promise.reject(new AbortedError());
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const handleAbort = () => { if (onAbort)
|
|
66
|
+
resolve(onAbort());
|
|
67
|
+
else
|
|
68
|
+
reject(new AbortedError()); };
|
|
69
|
+
signal.addEventListener("abort", handleAbort, { once: true });
|
|
70
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", handleAbort));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export async function mapWithConcurrency(items, limit, fn, signal) {
|
|
74
|
+
const results = [];
|
|
75
|
+
for (let i = 0; i < items.length; i += limit) {
|
|
76
|
+
if (signal?.aborted)
|
|
77
|
+
break;
|
|
78
|
+
const chunk = items.slice(i, i + limit);
|
|
79
|
+
results.push(...(await Promise.all(chunk.map((item) => fn(item)))));
|
|
80
|
+
}
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
export async function withTimeoutFn(fn, ms, signal, timeoutMessage, otherError) {
|
|
84
|
+
const timed = withTimeoutSignal(signal, ms);
|
|
85
|
+
try {
|
|
86
|
+
return await fn(timed);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
if (isTimeout(timed))
|
|
90
|
+
throw new Error(timeoutMessage);
|
|
91
|
+
throw otherError ? otherError(e) : e;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const MAX_PROCESS_BUFFER_MB = 10;
|
|
2
|
+
export declare const MAX_WEB_FETCH_MB = 10;
|
|
3
|
+
export declare const MAX_FILE_READ_MB = 20;
|
|
4
|
+
export declare const CALL_TIMEOUT_MS = 300000;
|
|
5
|
+
export declare const REQUEST_TIMEOUT_MS = 60000;
|
|
6
|
+
export declare const MCP_CONNECT_TIMEOUT_MS = 30000;
|
|
7
|
+
export declare const NO_OUTPUT = "(no output)";
|
|
8
|
+
export declare const INTERRUPTED_TOOL_CONTENT = "(interrupted)";
|
|
9
|
+
export declare const NO_MATCHES = "(no matches)";
|
|
10
|
+
export declare const NOT_EXECUTED_PREFIX = "(not executed: ";
|
|
11
|
+
export declare const MAX_SUMMARY_LENGTH = 75;
|
|
12
|
+
export declare const MAX_ARGS_SUMMARY_LENGTH = 300;
|
|
13
|
+
export declare const DEFAULT_STALL_THRESHOLD = 3;
|
|
14
|
+
export declare const DEFAULT_MAX_TURNS = 50;
|
|
15
|
+
export declare const DEFAULT_FILE_READ_LIMIT = 2000;
|
|
16
|
+
export declare const DEFAULT_GREP_LIMIT = 200;
|
|
17
|
+
export declare const MAX_PARALLEL_TOOL_CALLS = 8;
|
|
18
|
+
export declare const SKILL_TOOL_NAME: "Skill";
|
|
19
|
+
export declare const LLM_MAX_RETRIES = 3;
|
|
20
|
+
export declare const WEB_FETCH_RETRIES = 2;
|
|
21
|
+
export declare const DEFAULT_THINKING_EFFORT: "high";
|
|
22
|
+
export declare const DEFAULT_BACKEND: "completions";
|
|
23
|
+
export declare const DEFAULT_MAX_INPUT_TOKENS = 1000000;
|
|
24
|
+
export declare const DEFAULT_MAX_OUTPUT_TOKENS = 128000;
|
|
25
|
+
export declare function mbToBytes(mb: number): number;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const MAX_PROCESS_BUFFER_MB = 10;
|
|
2
|
+
export const MAX_WEB_FETCH_MB = 10;
|
|
3
|
+
export const MAX_FILE_READ_MB = 20;
|
|
4
|
+
export const CALL_TIMEOUT_MS = 300_000;
|
|
5
|
+
export const REQUEST_TIMEOUT_MS = 60_000;
|
|
6
|
+
export const MCP_CONNECT_TIMEOUT_MS = 30_000;
|
|
7
|
+
export const NO_OUTPUT = "(no output)";
|
|
8
|
+
export const INTERRUPTED_TOOL_CONTENT = "(interrupted)";
|
|
9
|
+
export const NO_MATCHES = "(no matches)";
|
|
10
|
+
export const NOT_EXECUTED_PREFIX = "(not executed: ";
|
|
11
|
+
export const MAX_SUMMARY_LENGTH = 75;
|
|
12
|
+
export const MAX_ARGS_SUMMARY_LENGTH = 300;
|
|
13
|
+
export const DEFAULT_STALL_THRESHOLD = 3;
|
|
14
|
+
export const DEFAULT_MAX_TURNS = 50;
|
|
15
|
+
export const DEFAULT_FILE_READ_LIMIT = 2000;
|
|
16
|
+
export const DEFAULT_GREP_LIMIT = 200;
|
|
17
|
+
export const MAX_PARALLEL_TOOL_CALLS = 8;
|
|
18
|
+
export const SKILL_TOOL_NAME = "Skill";
|
|
19
|
+
export const LLM_MAX_RETRIES = 3;
|
|
20
|
+
export const WEB_FETCH_RETRIES = 2;
|
|
21
|
+
export const DEFAULT_THINKING_EFFORT = "high";
|
|
22
|
+
export const DEFAULT_BACKEND = "completions";
|
|
23
|
+
export const DEFAULT_MAX_INPUT_TOKENS = 1_000_000;
|
|
24
|
+
export const DEFAULT_MAX_OUTPUT_TOKENS = 128_000;
|
|
25
|
+
export function mbToBytes(mb) {
|
|
26
|
+
return mb * 1024 * 1024;
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class Emitter {
|
|
2
|
+
listeners = new Set();
|
|
3
|
+
subscribe(listener) {
|
|
4
|
+
this.listeners.add(listener);
|
|
5
|
+
return () => this.listeners.delete(listener);
|
|
6
|
+
}
|
|
7
|
+
notify(...args) {
|
|
8
|
+
for (const l of this.listeners) {
|
|
9
|
+
try {
|
|
10
|
+
l(...args);
|
|
11
|
+
}
|
|
12
|
+
catch { /* isolate listener errors */ }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
export function tryReadFileText(path) {
|
|
4
|
+
if (existsSync(path)) {
|
|
5
|
+
const content = readFileSync(path, "utf-8").trim();
|
|
6
|
+
if (content)
|
|
7
|
+
return content;
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
export function resolveRequiredPath(args, cwd) {
|
|
12
|
+
const path = args.path;
|
|
13
|
+
if (typeof path !== "string" || !path)
|
|
14
|
+
throw new Error("path is required");
|
|
15
|
+
return resolve(cwd, path);
|
|
16
|
+
}
|
|
17
|
+
export function resolveOptionalPath(args, cwd) {
|
|
18
|
+
return resolve(cwd, args.path || "");
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function htmlToMarkdown(html: string): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import TurndownService from "turndown";
|
|
2
|
+
const turndown = new TurndownService({
|
|
3
|
+
headingStyle: "atx",
|
|
4
|
+
hr: "---",
|
|
5
|
+
bulletListMarker: "-",
|
|
6
|
+
codeBlockStyle: "fenced",
|
|
7
|
+
emDelimiter: "*",
|
|
8
|
+
strongDelimiter: "**",
|
|
9
|
+
linkStyle: "inlined",
|
|
10
|
+
});
|
|
11
|
+
turndown.remove(["script", "style", "title", "meta", "head", "noscript", "template", "link", "base"]);
|
|
12
|
+
export function htmlToMarkdown(html) {
|
|
13
|
+
return turndown.turndown(html);
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { AbortedError, backoffDelay, isAbortError, mapWithConcurrency, withAbort, withRetry, withTimeout, withTimeoutFn, type RetryOptions } from "./async.js";
|
|
2
|
+
export { DEFAULT_BACKEND, DEFAULT_MAX_INPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_THINKING_EFFORT } from "./constants.js";
|
|
3
|
+
export { tryReadFileText } from "./file.js";
|
|
4
|
+
export { htmlToMarkdown } from "./html.js";
|
|
5
|
+
export { netFetch } from "./net.js";
|
|
6
|
+
export { type ProcessResult, runProcess } from "./subprocess.js";
|
|
7
|
+
export { formatCompactNumber, formatDuration, getTextBytes, summarizeText, toErrorMessage } from "./text.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { AbortedError, backoffDelay, isAbortError, mapWithConcurrency, withAbort, withRetry, withTimeout, withTimeoutFn } from "./async.js";
|
|
2
|
+
export { DEFAULT_BACKEND, DEFAULT_MAX_INPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_THINKING_EFFORT } from "./constants.js";
|
|
3
|
+
export { tryReadFileText } from "./file.js";
|
|
4
|
+
export { htmlToMarkdown } from "./html.js";
|
|
5
|
+
export { netFetch } from "./net.js";
|
|
6
|
+
export { runProcess } from "./subprocess.js";
|
|
7
|
+
export { formatCompactNumber, formatDuration, getTextBytes, summarizeText, toErrorMessage } from "./text.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function netFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
package/dist/util/net.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ProxyAgent } from "undici";
|
|
2
|
+
let proxyAgent;
|
|
3
|
+
let noProxyList;
|
|
4
|
+
let proxyInitialized = false;
|
|
5
|
+
function ensureProxy() {
|
|
6
|
+
proxyInitialized = true;
|
|
7
|
+
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy;
|
|
8
|
+
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
|
9
|
+
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
|
|
10
|
+
const proxyUrl = httpsProxy || httpProxy;
|
|
11
|
+
proxyAgent = proxyUrl ? new ProxyAgent(proxyUrl) : null;
|
|
12
|
+
noProxyList = noProxy?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
export async function netFetch(input, init) {
|
|
15
|
+
if (!proxyInitialized)
|
|
16
|
+
ensureProxy();
|
|
17
|
+
if (!proxyAgent)
|
|
18
|
+
return fetch(input, init);
|
|
19
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
20
|
+
if (noProxyList) {
|
|
21
|
+
const hostname = new URL(url).hostname;
|
|
22
|
+
const shouldBypass = noProxyList.some((p) => {
|
|
23
|
+
if (p.startsWith("."))
|
|
24
|
+
return hostname.endsWith(p);
|
|
25
|
+
return hostname === p || hostname.endsWith("." + p);
|
|
26
|
+
});
|
|
27
|
+
if (shouldBypass)
|
|
28
|
+
return fetch(input, init);
|
|
29
|
+
}
|
|
30
|
+
return fetch(input, { ...init, dispatcher: proxyAgent });
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function formatRipgrepOutput(lines: string[], truncated: boolean, emptyText: string): string;
|
|
2
|
+
export declare function ripgrepResultSummary(word: "file" | "match", result: {
|
|
3
|
+
content: string;
|
|
4
|
+
isError?: boolean;
|
|
5
|
+
}, failText: string, noMatchesText: string): string;
|
|
6
|
+
export interface RipgrepLinesResult {
|
|
7
|
+
lines: string[];
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function runRipgrepLines(args: string[], cwd: string, signal?: AbortSignal, limit?: number): Promise<RipgrepLinesResult>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { rgPath } from "@vscode/ripgrep";
|
|
2
|
+
import { runProcess } from "./subprocess.js";
|
|
3
|
+
import { NO_MATCHES, REQUEST_TIMEOUT_MS } from "./constants.js";
|
|
4
|
+
import { countNonEmptyLines, summaryCount } from "./text.js";
|
|
5
|
+
const TRUNCATION_MARKER = "(output truncated)";
|
|
6
|
+
export function formatRipgrepOutput(lines, truncated, emptyText) {
|
|
7
|
+
if (!lines.length)
|
|
8
|
+
return emptyText;
|
|
9
|
+
const out = lines.join("\n");
|
|
10
|
+
return truncated ? out + "\n" + TRUNCATION_MARKER : out;
|
|
11
|
+
}
|
|
12
|
+
export function ripgrepResultSummary(word, result, failText, noMatchesText) {
|
|
13
|
+
if (result.isError)
|
|
14
|
+
return failText;
|
|
15
|
+
if (result.content === NO_MATCHES)
|
|
16
|
+
return noMatchesText;
|
|
17
|
+
const lines = countNonEmptyLines(result.content);
|
|
18
|
+
const count = lines - (result.content.endsWith(TRUNCATION_MARKER) ? 1 : 0);
|
|
19
|
+
return summaryCount(word, count, false, failText);
|
|
20
|
+
}
|
|
21
|
+
export async function runRipgrepLines(args, cwd, signal, limit) {
|
|
22
|
+
const rgArgs = ["--hidden", "--path-separator", "/", "-g", "!.git/**", "-g", "!node_modules/**", ...args];
|
|
23
|
+
const r = await runProcess(rgPath, rgArgs, { cwd, timeout: REQUEST_TIMEOUT_MS }, signal);
|
|
24
|
+
if (!r.truncated && (r.error || (r.status !== 0 && r.status !== 1))) {
|
|
25
|
+
throw r.error ?? new Error((r.stderr || "").trim() || `ripgrep exited with ${r.status}`);
|
|
26
|
+
}
|
|
27
|
+
let kept = r.stdout.split("\n").filter(Boolean);
|
|
28
|
+
let truncated = r.truncated === true;
|
|
29
|
+
if (limit !== undefined && kept.length > limit) {
|
|
30
|
+
kept = kept.slice(0, limit);
|
|
31
|
+
truncated = true;
|
|
32
|
+
}
|
|
33
|
+
return { lines: kept.map((f) => f.replace(/^\.\//, "")), truncated };
|
|
34
|
+
}
|