@vietor/easy-agent 0.3.1 → 0.4.1
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/README.md +31 -22
- package/dist/cli.js +0 -0
- package/dist/cmds/builtin.js +8 -0
- package/dist/config.js +1 -1
- package/dist/main.js +14 -40
- package/dist/tui/App.js +25 -12
- package/dist/tui/AppHeader.js +5 -4
- package/dist/tui/LogList.js +9 -0
- package/dist/tui/PromptOrCommandInput.js +1 -4
- package/dist/tui/TodoView.js +19 -0
- package/package.json +18 -21
- package/dist/cmds/registry.js +0 -30
- package/dist/cmds/types.js +0 -1
- package/dist/core/agent.js +0 -134
- package/dist/core/conversation.js +0 -82
- package/dist/core/logstore.js +0 -49
- package/dist/core/session.js +0 -181
- package/dist/llm/client.js +0 -80
- package/dist/llm/types.js +0 -1
- package/dist/mcp/client.js +0 -81
- package/dist/mcp/server.js +0 -124
- package/dist/skills/loader.js +0 -43
- package/dist/skills/types.js +0 -1
- package/dist/tools/ask_user.js +0 -24
- package/dist/tools/file_edit.js +0 -47
- package/dist/tools/file_read.js +0 -43
- package/dist/tools/file_write.js +0 -22
- package/dist/tools/glob.js +0 -29
- package/dist/tools/grep.js +0 -73
- package/dist/tools/registry.js +0 -52
- package/dist/tools/shell.js +0 -34
- package/dist/tools/types.js +0 -1
- package/dist/tools/web_fetch.js +0 -141
- package/dist/util/async.js +0 -58
- package/dist/util/fs.js +0 -17
- package/dist/util/package.js +0 -26
- package/dist/util/process.js +0 -43
- package/dist/util/ripgrep.js +0 -17
package/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @vietor/easy-agent
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[npm]: https://img.shields.io/npm/v/@vietor/easy-agent.svg?style=flat-square
|
|
6
|
-
|
|
7
|
-
An autonomous coding agent in the terminal.
|
|
3
|
+
Terminal-based AI agent CLI with a conversational TUI, powered by `@vietor/easy-agent-core`.
|
|
8
4
|
|
|
9
5
|
## Requirements
|
|
10
6
|
|
|
@@ -12,7 +8,7 @@ An autonomous coding agent in the terminal.
|
|
|
12
8
|
|
|
13
9
|
## Install
|
|
14
10
|
|
|
15
|
-
Install globally
|
|
11
|
+
Install globally:
|
|
16
12
|
|
|
17
13
|
```bash
|
|
18
14
|
npm install -g @vietor/easy-agent
|
|
@@ -31,18 +27,18 @@ Create `~/.easy-agent.json` in your home directory:
|
|
|
31
27
|
```json
|
|
32
28
|
{
|
|
33
29
|
"llm": {
|
|
34
|
-
"baseUrl": "https://api.
|
|
30
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
35
31
|
"apiKey": "your-api-key",
|
|
36
|
-
"model": "
|
|
32
|
+
"model": "gpt-4o"
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
35
|
```
|
|
40
36
|
|
|
41
37
|
`llm.baseUrl`, `llm.apiKey`, and `llm.model` are all required. Point `baseUrl` at any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, local servers, etc.) and set `model` to a model that endpoint serves.
|
|
42
38
|
|
|
43
|
-
###
|
|
39
|
+
### MCP servers (optional)
|
|
44
40
|
|
|
45
|
-
Add an `mcpServers` map to expose external tools through the Model Context Protocol. Each entry is either a local process (stdio) or a remote endpoint (
|
|
41
|
+
Add an `mcpServers` map to expose external tools through the Model Context Protocol. Each entry is either a local process (stdio) or a remote endpoint (Streamable HTTP):
|
|
46
42
|
|
|
47
43
|
```json
|
|
48
44
|
{
|
|
@@ -52,7 +48,7 @@ Add an `mcpServers` map to expose external tools through the Model Context Proto
|
|
|
52
48
|
"command": "npx",
|
|
53
49
|
"args": ["chrome-devtools-mcp@latest", "--auto-connect", "--accept-insecure-certs"]
|
|
54
50
|
},
|
|
55
|
-
"
|
|
51
|
+
"api-server": {
|
|
56
52
|
"type": "http",
|
|
57
53
|
"url": "https://mcp.example.com/mcp",
|
|
58
54
|
"headers": { "Authorization": "Bearer your-token" }
|
|
@@ -61,9 +57,9 @@ Add an `mcpServers` map to expose external tools through the Model Context Proto
|
|
|
61
57
|
}
|
|
62
58
|
```
|
|
63
59
|
|
|
64
|
-
A stdio server omits `type` (or sets `"stdio"`); only `command` is required, `args` and `env` are optional. A remote server sets `type` to `"
|
|
60
|
+
A stdio server omits `type` (or sets `"stdio"`); only `command` is required, `args` and `env` are optional. A remote server sets `type` to `"http"` with a required `url`; `headers` is optional and sent with every request (use it for static auth tokens). Either entry accepts an optional `enabled` (defaults to `true`); set `false` to keep a server configured but skip starting it. MCP tools become available to the agent as `MCP__<server>__<tool>`. If a server fails to connect within 30s, it is disabled and the error is shown in the TUI — for stdio servers the captured stderr tail is included to help diagnose startup failures — and the rest keep running.
|
|
65
61
|
|
|
66
|
-
###
|
|
62
|
+
### Agent instructions (optional)
|
|
67
63
|
|
|
68
64
|
Easy Agent reads instructions files and appends them to the system prompt, so you can set persistent rules, conventions, or preferences. It looks in two places:
|
|
69
65
|
|
|
@@ -72,7 +68,7 @@ Easy Agent reads instructions files and appends them to the system prompt, so yo
|
|
|
72
68
|
|
|
73
69
|
Both global and project files are loaded and concatenated into the system prompt if they exist.
|
|
74
70
|
|
|
75
|
-
###
|
|
71
|
+
### Skills (optional)
|
|
76
72
|
|
|
77
73
|
Skills are reusable prompts that register themselves as slash commands. Create a subdirectory for each skill under `~/.agents/skills/` (or `~/.claude/skills/`) with a `SKILL.md` file:
|
|
78
74
|
|
|
@@ -107,7 +103,7 @@ easy-agent
|
|
|
107
103
|
|
|
108
104
|
Type a prompt and press Enter. The agent streams its reply and calls tools as needed, showing each tool call and a one-line preview of its result. It iterates until the task is done (capped at 50 tool rounds per turn).
|
|
109
105
|
|
|
110
|
-
Built-in tools
|
|
106
|
+
### Built-in tools
|
|
111
107
|
|
|
112
108
|
- **Shell** — run shell commands using this platform's native syntax.
|
|
113
109
|
- **FileRead** — read a file with line numbers; supports `offset`/`limit` for paging large files.
|
|
@@ -117,11 +113,24 @@ Built-in tools:
|
|
|
117
113
|
- **Grep** — search file contents by regex with `glob`/`type` filters, context lines, case-insensitive, and `files_with_matches`/`count` output modes.
|
|
118
114
|
- **WebFetch** — fetch a URL as markdown or text.
|
|
119
115
|
- **AskUser** — ask the user a question and wait for their answer.
|
|
116
|
+
- **TodoWrite** — track multi-step work as a task list (pending / in_progress / completed), shown live as a panel in the TUI.
|
|
117
|
+
|
|
118
|
+
### Slash commands
|
|
120
119
|
|
|
121
|
-
|
|
120
|
+
| Command | Description |
|
|
121
|
+
|---------|-------------|
|
|
122
|
+
| `/mcp` | List linked MCP servers, their status, and exposed tools |
|
|
123
|
+
| `/clear` | Reset the conversation |
|
|
124
|
+
| `/compact` | Compress the conversation into a summary to free context |
|
|
125
|
+
| `/export` | Save the current conversation to `conversation-{timestamp}.jsonl` |
|
|
126
|
+
| `/quit` or `/exit` | Leave the app |
|
|
122
127
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
+
## Build from source
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
git clone <repo-url>
|
|
132
|
+
cd easy-agent
|
|
133
|
+
pnpm install
|
|
134
|
+
pnpm build # build core → CLI
|
|
135
|
+
pnpm --filter @vietor/easy-agent dev # hot-reload dev mode
|
|
136
|
+
```
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/cmds/builtin.js
CHANGED
package/dist/config.js
CHANGED
|
@@ -16,7 +16,7 @@ const StdioServerConfig = z.object({
|
|
|
16
16
|
enabled: z.boolean().optional(),
|
|
17
17
|
});
|
|
18
18
|
const RemoteServerConfig = z.object({
|
|
19
|
-
type: z.enum(["
|
|
19
|
+
type: z.enum(["http"]),
|
|
20
20
|
url: z.string().url(),
|
|
21
21
|
headers: z.record(z.string(), z.string()).optional(),
|
|
22
22
|
enabled: z.boolean().optional(),
|
package/dist/main.js
CHANGED
|
@@ -1,29 +1,11 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadConfig } from "./config.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { ToolRegistry, registerBuiltinTools } from "./tools/registry.js";
|
|
7
|
-
import { CommandRegistry, registerBuiltinCommands } from "./cmds/registry.js";
|
|
8
|
-
import { tryLoadSkills } from "./skills/loader.js";
|
|
9
|
-
import { tryReadFileText, readFirstFileContent } from "./util/fs.js";
|
|
10
|
-
import { MCPServers } from "./mcp/server.js";
|
|
4
|
+
import { tryLoadSkills, tryReadFileText, readFirstFileContent, startSession, } from "@vietor/easy-agent-core";
|
|
5
|
+
import { builtinCommands } from "./cmds/builtin.js";
|
|
11
6
|
import { startApp } from "./tui/App.js";
|
|
12
7
|
const SYSTEM_PROMPT_BASE = [
|
|
13
|
-
"You are Easy Agent, an autonomous
|
|
14
|
-
`Environment:
|
|
15
|
-
- Platform: ${process.platform}
|
|
16
|
-
- Working directory: ${process.cwd()}`,
|
|
17
|
-
[
|
|
18
|
-
"Tool use:",
|
|
19
|
-
"- Prefer dedicated tools (FileRead, FileWrite, FileEdit, Glob, Grep, WebFetch) over the Shell tool when they fit the task.",
|
|
20
|
-
"- Read a file before editing it; make minimal, surgical changes that match the surrounding code style.",
|
|
21
|
-
"- Reference code as file_path:line_number.",
|
|
22
|
-
"- When a decision belongs to the user, call AskUser and wait for the answer rather than listing options in prose. Ask when there are multiple reasonable approaches, an irreversible or consequential action, or the request is ambiguous; when you have enough to proceed, act without asking.",
|
|
23
|
-
...(process.platform === "linux"
|
|
24
|
-
? ["- For privileged shell commands, use `sudo -n` (non-interactive); if it reports a password is required, do not retry - surface the command for the user to run manually."]
|
|
25
|
-
: []),
|
|
26
|
-
].join("\n"),
|
|
8
|
+
"You are Easy Agent, an autonomous assistant. You complete tasks by calling tools, inspecting their results, and iterating until the work is done.",
|
|
27
9
|
`Output:
|
|
28
10
|
- Be concise and use GitHub-flavored markdown.
|
|
29
11
|
- State what you did and stop once the task is complete; report outcomes faithfully.
|
|
@@ -31,31 +13,23 @@ const SYSTEM_PROMPT_BASE = [
|
|
|
31
13
|
].join("\n\n");
|
|
32
14
|
export async function main() {
|
|
33
15
|
const config = loadConfig();
|
|
34
|
-
const llm = new LLMClient(config.llm);
|
|
35
|
-
const tools = new ToolRegistry();
|
|
36
|
-
registerBuiltinTools(tools);
|
|
37
|
-
const commands = new CommandRegistry();
|
|
38
|
-
registerBuiltinCommands(commands);
|
|
39
|
-
const mcp = new MCPServers(tools);
|
|
40
|
-
mcp
|
|
41
|
-
.connect(config.mcpServers)
|
|
42
|
-
.catch((e) => mcp.report(`MCP connect failed: ${e.message}`));
|
|
43
16
|
const globalSkills = readFirstFileContent([join(homedir(), ".agents", "skills"), join(homedir(), ".claude", "skills")], tryLoadSkills);
|
|
44
|
-
if (globalSkills) {
|
|
45
|
-
globalSkills.forEach((skill) => commands.register({
|
|
46
|
-
name: skill.name,
|
|
47
|
-
description: skill.description ?? skill.name,
|
|
48
|
-
execute: async (_, host) => {
|
|
49
|
-
await host.runSkill(skill);
|
|
50
|
-
},
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
17
|
const globalPrompt = readFirstFileContent([join(homedir(), ".agents", "AGENTS.md"), join(homedir(), ".claude", "CLAUDE.md")], tryReadFileText);
|
|
54
18
|
const projectPrompt = readFirstFileContent([join(process.cwd(), "AGENTS.md"), join(process.cwd(), "CLAUDE.md")], tryReadFileText);
|
|
55
19
|
const systemPrompt = [SYSTEM_PROMPT_BASE, globalPrompt, projectPrompt]
|
|
56
20
|
.filter(Boolean)
|
|
57
21
|
.join("\n\n=================\n\n");
|
|
58
|
-
const session =
|
|
22
|
+
const session = await startSession({
|
|
23
|
+
systemPrompt,
|
|
24
|
+
llmConfig: config.llm,
|
|
25
|
+
mcpServers: config.mcpServers,
|
|
26
|
+
skills: globalSkills,
|
|
27
|
+
commands: builtinCommands,
|
|
28
|
+
enableTools: {
|
|
29
|
+
askUser: true,
|
|
30
|
+
todoWrite: true
|
|
31
|
+
}
|
|
32
|
+
});
|
|
59
33
|
const app = startApp(session);
|
|
60
34
|
await app.waitUntilExit().finally(() => session.dispose());
|
|
61
35
|
}
|
package/dist/tui/App.js
CHANGED
|
@@ -2,24 +2,25 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { Box, render, Text, useApp, useInput } from "ink";
|
|
4
4
|
import { Markdown } from "./components/Markdown.js";
|
|
5
|
-
import {
|
|
5
|
+
import { LogList } from "./LogList.js";
|
|
6
|
+
import { TodoView } from "./TodoView.js";
|
|
6
7
|
import { AppHeader } from "./AppHeader.js";
|
|
7
8
|
import { PromptOrCommandInput } from "./PromptOrCommandInput.js";
|
|
8
9
|
import { QuestionView } from "./QuestionView.js";
|
|
9
10
|
import { Spinner } from "./Spinner.js";
|
|
10
11
|
import { compactDisplay } from "../util/format.js";
|
|
11
|
-
const STREAM_FRAME_MS =
|
|
12
|
+
const STREAM_FRAME_MS = 120;
|
|
12
13
|
export function App({ session }) {
|
|
13
14
|
const { exit } = useApp();
|
|
14
|
-
useSyncExternalStore(session.subscribe, session.getSnapshot);
|
|
15
|
+
const version = useSyncExternalStore(session.subscribe, session.getSnapshot);
|
|
15
16
|
const [running, setRunning] = useState(false);
|
|
16
|
-
const [
|
|
17
|
-
const [
|
|
18
|
-
const [,
|
|
17
|
+
const [runElapsed, setRunElapsed] = useState(0);
|
|
18
|
+
const [runUsage, setRunUsage] = useState({ prompt: 0, completion: 0 });
|
|
19
|
+
const [streamingText, setStreamingText] = useState("");
|
|
19
20
|
const streamingRef = useRef("");
|
|
20
21
|
const renderTimerRef = useRef(undefined);
|
|
21
22
|
const allCmds = useMemo(() => session.commandSchemas, [session]);
|
|
22
|
-
const pendingQuestion = session.logEntries.find((e) => e.kind === "question" && e.answer === null);
|
|
23
|
+
const pendingQuestion = useMemo(() => session.logEntries.find((e) => e.kind === "question" && e.answer === null), [session, version]);
|
|
23
24
|
useEffect(() => {
|
|
24
25
|
session.setCallbacks({
|
|
25
26
|
onStreaming: (text) => {
|
|
@@ -27,8 +28,8 @@ export function App({ session }) {
|
|
|
27
28
|
scheduleStreamingRender();
|
|
28
29
|
},
|
|
29
30
|
onRunStateChange: (r) => setRunning(r),
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
onRunElapsedChange: (s) => setRunElapsed(s),
|
|
32
|
+
onRunUsageChange: (p, c) => setRunUsage({ prompt: p, completion: c }),
|
|
32
33
|
});
|
|
33
34
|
}, []);
|
|
34
35
|
const scheduleStreamingRender = () => {
|
|
@@ -36,7 +37,7 @@ export function App({ session }) {
|
|
|
36
37
|
return;
|
|
37
38
|
renderTimerRef.current = setTimeout(() => {
|
|
38
39
|
renderTimerRef.current = undefined;
|
|
39
|
-
|
|
40
|
+
setStreamingText(streamingRef.current);
|
|
40
41
|
}, STREAM_FRAME_MS);
|
|
41
42
|
};
|
|
42
43
|
useInput((_input, key) => {
|
|
@@ -66,9 +67,21 @@ export function App({ session }) {
|
|
|
66
67
|
await session.startPrompt(text);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
+
let runningView = null;
|
|
71
|
+
if (running) {
|
|
72
|
+
if (pendingQuestion) {
|
|
73
|
+
runningView = (_jsx(QuestionView, { question: pendingQuestion, onAnswer: (ans) => session.submitAnswer(pendingQuestion.id, ans) }));
|
|
74
|
+
}
|
|
75
|
+
else if (streamingText) {
|
|
76
|
+
runningView = (_jsx(Box, { marginTop: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(Markdown, { color: "green", children: streamingText }) }));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
runningView = (_jsx(Box, { marginTop: 1, paddingLeft: 1, children: _jsx(Spinner, { label: "thinking", elapsed: runElapsed, promptTokens: runUsage.prompt, completionTokens: runUsage.completion }) }));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return (_jsxs(Box, { flexDirection: "column", minWidth: 80, children: [_jsx(AppHeader, {}), _jsx(LogList, { session: session }), session.todos.length > 0 ? _jsx(TodoView, { todos: session.todos }) : null, runningView, !running ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["[CTX ", compactDisplay(session.contextTokens), "] \u00B7 ESC to stop \u00B7 type / for commands"] }), _jsx(PromptOrCommandInput, { commands: allCmds, onCommand: handleCommand, onPrompt: handlePrompt })] })) : null] }));
|
|
70
83
|
}
|
|
71
84
|
export function startApp(session) {
|
|
72
85
|
process.stdout.write("[2J[H");
|
|
73
|
-
return render(_jsx(App, { session: session }), { exitOnCtrlC: false });
|
|
86
|
+
return render(_jsx(App, { session: session }), { exitOnCtrlC: false, incrementalRendering: true });
|
|
74
87
|
}
|
package/dist/tui/AppHeader.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
2
3
|
import { Box, Text } from "ink";
|
|
3
|
-
import { getPackageInfo } from "
|
|
4
|
-
export function AppHeader() {
|
|
4
|
+
import { getPackageInfo } from "@vietor/easy-agent-core";
|
|
5
|
+
export const AppHeader = memo(function AppHeader() {
|
|
5
6
|
const pkginfo = getPackageInfo();
|
|
6
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
7
|
-
}
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "red", bold: true, children: "Easy Agent" }), _jsxs(Text, { dimColor: true, children: [" v", pkginfo.version] })] }), _jsx(Text, { dimColor: true, children: process.cwd() })] }));
|
|
8
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useSyncExternalStore } from "react";
|
|
3
|
+
import { Box } from "ink";
|
|
4
|
+
import { LogView } from "./LogView.js";
|
|
5
|
+
export const LogList = memo(function LogList({ session }) {
|
|
6
|
+
useSyncExternalStore(session.subscribe, session.getSnapshot);
|
|
7
|
+
const entries = session.logEntries;
|
|
8
|
+
return (_jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: entries.map((entry, i) => (_jsx(LogView, { entry: entry }, i))) }));
|
|
9
|
+
});
|
|
@@ -16,10 +16,7 @@ export function PromptOrCommandInput({ commands, onCommand, onPrompt }) {
|
|
|
16
16
|
}, [showMenu, prefix, commands]);
|
|
17
17
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
18
18
|
useEffect(() => {
|
|
19
|
-
|
|
20
|
-
setSelectedIndex(0);
|
|
21
|
-
else if (selectedIndex >= filtered.length)
|
|
22
|
-
setSelectedIndex(Math.max(0, filtered.length - 1));
|
|
19
|
+
setSelectedIndex((prev) => Math.max(0, Math.min(prev, Math.max(0, filtered.length - 1))));
|
|
23
20
|
}, [filtered.length]);
|
|
24
21
|
useInput((_input, key) => {
|
|
25
22
|
if (key.upArrow) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
const ICONS = {
|
|
5
|
+
pending: "○",
|
|
6
|
+
in_progress: "◐",
|
|
7
|
+
completed: "✓",
|
|
8
|
+
};
|
|
9
|
+
const COLORS = {
|
|
10
|
+
pending: "gray",
|
|
11
|
+
in_progress: "yellow",
|
|
12
|
+
completed: "green",
|
|
13
|
+
};
|
|
14
|
+
export const TodoView = memo(function TodoView({ todos }) {
|
|
15
|
+
if (todos.length === 0)
|
|
16
|
+
return null;
|
|
17
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { dimColor: true, children: `Tasks [${done}/${todos.length}]` }), todos.map((t, i) => (_jsx(Text, { color: COLORS[t.status], strikethrough: t.status === "completed", children: `${ICONS[t.status]} ${t.content}` }, i)))] }));
|
|
19
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vietor/easy-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,40 +9,37 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
|
-
"
|
|
13
|
-
"build": "tsc",
|
|
14
|
-
"start": "tsc && node dist/cli.js",
|
|
15
|
-
"dev": "tsx src/cli.ts",
|
|
16
|
-
"prepublishOnly": "tsc"
|
|
17
|
-
},
|
|
18
|
-
"keywords": [
|
|
19
|
-
"ai",
|
|
20
|
-
"cli"
|
|
21
|
-
],
|
|
22
|
-
"author": "",
|
|
12
|
+
"description": "Terminal-based AI agent CLI with conversational TUI (Ink/React) powered by easy-agent-core",
|
|
23
13
|
"license": "MIT",
|
|
24
|
-
"
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/vietor/easy-agent.git"
|
|
17
|
+
},
|
|
25
18
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
-
"@vscode/ripgrep": "^1.18.0",
|
|
28
|
-
"htmlparser2": "^12.0.0",
|
|
29
19
|
"ink": "^7.1.0",
|
|
30
20
|
"ink-text-input": "^6.0.0",
|
|
31
21
|
"marked": "^18.0.5",
|
|
32
|
-
"openai": "^6.45.0",
|
|
33
22
|
"react": "^19.2.7",
|
|
34
23
|
"string-width": "^8.2.1",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
24
|
+
"zod": "^4.4.3",
|
|
25
|
+
"@vietor/easy-agent-core": "0.4.1"
|
|
37
26
|
},
|
|
38
27
|
"devDependencies": {
|
|
39
28
|
"@types/node": "^22.0.0",
|
|
40
29
|
"@types/react": "^19.2.17",
|
|
41
|
-
"@types/turndown": "^5.0.6",
|
|
42
30
|
"tsx": "^4.23.0",
|
|
43
31
|
"typescript": "^6.0.3"
|
|
44
32
|
},
|
|
45
33
|
"engines": {
|
|
46
34
|
"node": ">=22.0.0"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"ai",
|
|
38
|
+
"cli"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"dev": "tsx src/cli.ts",
|
|
43
|
+
"start": "node dist/cli.js"
|
|
47
44
|
}
|
|
48
|
-
}
|
|
45
|
+
}
|
package/dist/cmds/registry.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { exitCommand, clearCommand, mcpCommand, compactCommand, exportCommand } from "./builtin.js";
|
|
2
|
-
export class CommandRegistry {
|
|
3
|
-
commands = new Map();
|
|
4
|
-
register(command) {
|
|
5
|
-
this.commands.set(command.name, command);
|
|
6
|
-
}
|
|
7
|
-
schemas() {
|
|
8
|
-
return [...this.commands.values()].map((t) => ({
|
|
9
|
-
name: t.name,
|
|
10
|
-
description: t.description,
|
|
11
|
-
}));
|
|
12
|
-
}
|
|
13
|
-
exists(name) {
|
|
14
|
-
return this.commands.has(name);
|
|
15
|
-
}
|
|
16
|
-
async execute(command, ctx, host) {
|
|
17
|
-
const cmd = this.commands.get(command);
|
|
18
|
-
if (!cmd) {
|
|
19
|
-
host.error(`unknown command: /${command}`);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
await cmd.execute(ctx, host);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
export function registerBuiltinCommands(commands) {
|
|
26
|
-
commands.register(exitCommand);
|
|
27
|
-
commands.register({ ...exitCommand, name: "quit" });
|
|
28
|
-
for (const t of [clearCommand, mcpCommand, compactCommand, exportCommand])
|
|
29
|
-
commands.register(t);
|
|
30
|
-
}
|
package/dist/cmds/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/core/agent.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { withAbort } from "../util/async.js";
|
|
2
|
-
const STALL_THRESHOLD = 3;
|
|
3
|
-
const MAX_TURNS = 50;
|
|
4
|
-
const COMPACT_PROMPT = "Summarize this conversation into a concise context summary. Preserve the user's goal, decisions made, files touched, and current progress. Write the summary in the same language the user used in the conversation. Begin your reply with \"Summary of conversation so far:\".";
|
|
5
|
-
export class Agent {
|
|
6
|
-
llm;
|
|
7
|
-
conversation;
|
|
8
|
-
tools;
|
|
9
|
-
ask;
|
|
10
|
-
constructor(llm, conversation, tools, ask) {
|
|
11
|
-
this.llm = llm;
|
|
12
|
-
this.conversation = conversation;
|
|
13
|
-
this.tools = tools;
|
|
14
|
-
this.ask = ask;
|
|
15
|
-
}
|
|
16
|
-
get contextTokens() {
|
|
17
|
-
return this.conversation.getEstimatedTokens();
|
|
18
|
-
}
|
|
19
|
-
clear() {
|
|
20
|
-
this.conversation.clear();
|
|
21
|
-
}
|
|
22
|
-
export() {
|
|
23
|
-
return this.conversation.export();
|
|
24
|
-
}
|
|
25
|
-
async compact(signal) {
|
|
26
|
-
const history = this.conversation.toLLM().slice(1);
|
|
27
|
-
if (history.length === 0)
|
|
28
|
-
return;
|
|
29
|
-
const request = [
|
|
30
|
-
...history,
|
|
31
|
-
{ role: "user", content: COMPACT_PROMPT },
|
|
32
|
-
];
|
|
33
|
-
const msg = await this.llm.chat({ messages: request, tools: [], signal });
|
|
34
|
-
this.conversation.compact(msg.content || "");
|
|
35
|
-
}
|
|
36
|
-
async run(userInput, onEvent, signal) {
|
|
37
|
-
await this.runTurn({ role: "user", content: userInput }, onEvent, signal);
|
|
38
|
-
}
|
|
39
|
-
async runSkill(skill, onEvent, signal) {
|
|
40
|
-
await this.runTurn({ role: "skill", name: skill.name, content: skill.prompt }, onEvent, signal);
|
|
41
|
-
}
|
|
42
|
-
async runTurn(msg, onEvent, signal) {
|
|
43
|
-
this.conversation.add(msg);
|
|
44
|
-
this.conversation.createSnapshot();
|
|
45
|
-
try {
|
|
46
|
-
await this.loop(onEvent, signal);
|
|
47
|
-
}
|
|
48
|
-
finally {
|
|
49
|
-
this.conversation.clearSnapshot();
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
async loop(onEvent, signal) {
|
|
53
|
-
await withAbort(async (aborted) => {
|
|
54
|
-
let lastSig = "";
|
|
55
|
-
let stall = 0;
|
|
56
|
-
let turns = 0;
|
|
57
|
-
while (true) {
|
|
58
|
-
let msg;
|
|
59
|
-
try {
|
|
60
|
-
msg = await this.llm.chat({
|
|
61
|
-
messages: this.conversation.toLLM(),
|
|
62
|
-
tools: this.tools.schemas(),
|
|
63
|
-
onDelta: (text) => onEvent?.({ type: "delta", text }),
|
|
64
|
-
onRetry: (attempt, max) => onEvent?.({ type: "retry", attempt, max }),
|
|
65
|
-
onUsage: (promptTokens, completionTokens) => onEvent?.({ type: "usage", promptTokens, completionTokens }),
|
|
66
|
-
signal,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
catch (e) {
|
|
70
|
-
if (aborted())
|
|
71
|
-
return;
|
|
72
|
-
onEvent?.({ type: "error", text: e.message });
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
this.conversation.add(msg);
|
|
76
|
-
if (!msg.tool_calls?.length)
|
|
77
|
-
return;
|
|
78
|
-
if (aborted())
|
|
79
|
-
return;
|
|
80
|
-
const sig = msg.tool_calls
|
|
81
|
-
.map((c) => `${c.function.name}:${c.function.arguments}`)
|
|
82
|
-
.join("|");
|
|
83
|
-
stall = sig === lastSig ? stall + 1 : 1;
|
|
84
|
-
lastSig = sig;
|
|
85
|
-
if (stall >= STALL_THRESHOLD) {
|
|
86
|
-
onEvent?.({ type: "error", text: "agent stalled: repeated identical tool calls" });
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (++turns >= MAX_TURNS) {
|
|
90
|
-
onEvent?.({ type: "error", text: `agent exceeded max turns (${MAX_TURNS})` });
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const results = await Promise.all(msg.tool_calls.map(async (call) => {
|
|
94
|
-
let args = {};
|
|
95
|
-
let argsError = "";
|
|
96
|
-
if (call.function.arguments) {
|
|
97
|
-
try {
|
|
98
|
-
args = JSON.parse(call.function.arguments);
|
|
99
|
-
}
|
|
100
|
-
catch (e) {
|
|
101
|
-
argsError = `Error: invalid arguments: ${e.message}`;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const summary = this.tools.summarize(call.function.name, args);
|
|
105
|
-
onEvent?.({ type: "tool_start", id: call.id, name: call.function.name, summary });
|
|
106
|
-
if (aborted())
|
|
107
|
-
return null;
|
|
108
|
-
const ctx = { signal, ask: this.ask };
|
|
109
|
-
const result = argsError
|
|
110
|
-
? { content: argsError, isError: true }
|
|
111
|
-
: await this.tools.execute(call.function.name, args, ctx);
|
|
112
|
-
if (aborted())
|
|
113
|
-
return null;
|
|
114
|
-
onEvent?.({ type: "tool_end", id: call.id, result: result.content, isError: result.isError });
|
|
115
|
-
return { id: call.id, content: result.content };
|
|
116
|
-
}));
|
|
117
|
-
if (aborted())
|
|
118
|
-
return;
|
|
119
|
-
for (const r of results) {
|
|
120
|
-
if (r)
|
|
121
|
-
this.conversation.add({ role: "tool", tool_call_id: r.id, content: r.content });
|
|
122
|
-
}
|
|
123
|
-
if (aborted())
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
}, {
|
|
127
|
-
signal,
|
|
128
|
-
onAbort: () => {
|
|
129
|
-
this.conversation.restoreFromSnapshot();
|
|
130
|
-
onEvent?.({ type: "interrupted" });
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|