@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.
@@ -1,82 +0,0 @@
1
- function estimateTokens(text) {
2
- if (!text)
3
- return 0;
4
- const cjk = (text.match(/[一-龥぀-ヿ가-힯]/g) || []).length;
5
- const wordChars = (text.match(/[a-zA-Z0-9']/g) || []).length;
6
- const words = (text.match(/[a-zA-Z0-9']+/g) || []).length;
7
- const rest = text.length - cjk - wordChars;
8
- return Math.ceil(cjk * 1.6 + words * 1.3 + rest * 0.3);
9
- }
10
- function messageText(msg) {
11
- const parts = [];
12
- if (typeof msg.content === "string")
13
- parts.push(msg.content);
14
- else if (Array.isArray(msg.content)) {
15
- for (const p of msg.content) {
16
- if (p.type === "text")
17
- parts.push(p.text);
18
- }
19
- }
20
- if ("tool_calls" in msg && msg.tool_calls) {
21
- for (const tc of msg.tool_calls) {
22
- if (tc.function?.name)
23
- parts.push(tc.function.name);
24
- if (tc.function?.arguments)
25
- parts.push(tc.function.arguments);
26
- }
27
- }
28
- return parts.join(" ");
29
- }
30
- export class Conversation {
31
- system;
32
- systemEstimateTokens;
33
- messages = [];
34
- estimatedTokens = 0;
35
- messagesSnapshot;
36
- estimatedTokensSnapshot = 0;
37
- constructor(system) {
38
- this.system = system;
39
- this.systemEstimateTokens = estimateTokens(system);
40
- this.messages.push({ role: "system", content: system });
41
- this.estimatedTokens = this.systemEstimateTokens;
42
- }
43
- getEstimatedTokens() {
44
- return this.estimatedTokens;
45
- }
46
- add(msg) {
47
- this.messages.push(msg);
48
- this.estimatedTokens += estimateTokens(messageText(msg));
49
- }
50
- toLLM() {
51
- return this.messages.map((m) => (m.role === "skill" ? { role: "user", name: m.name, content: m.content } : m));
52
- }
53
- export() {
54
- return this.messages.slice(1);
55
- }
56
- clear() {
57
- this.messages = [{ role: "system", content: this.system }];
58
- this.estimatedTokens = this.systemEstimateTokens;
59
- }
60
- compact(summary) {
61
- this.messages = [
62
- { role: "system", content: this.system },
63
- { role: "assistant", content: summary },
64
- ];
65
- this.estimatedTokens = this.systemEstimateTokens + estimateTokens(summary);
66
- }
67
- createSnapshot() {
68
- this.messagesSnapshot = this.messages.slice();
69
- this.estimatedTokensSnapshot = this.estimatedTokens;
70
- }
71
- restoreFromSnapshot() {
72
- const snap = this.messagesSnapshot;
73
- if (snap) {
74
- this.messages = snap.slice();
75
- this.estimatedTokens = this.estimatedTokensSnapshot;
76
- }
77
- }
78
- clearSnapshot() {
79
- this.messagesSnapshot = undefined;
80
- this.estimatedTokensSnapshot = 0;
81
- }
82
- }
@@ -1,49 +0,0 @@
1
- export class LogStore {
2
- entries = [];
3
- version = 0;
4
- listeners = new Set();
5
- getSnapshot = () => this.version;
6
- subscribe = (listener) => {
7
- this.listeners.add(listener);
8
- return () => this.listeners.delete(listener);
9
- };
10
- get all() {
11
- return this.entries;
12
- }
13
- append(entry) {
14
- this.entries.push(entry);
15
- this.version++;
16
- this.emit();
17
- }
18
- setResult(id, result, isError) {
19
- for (let i = this.entries.length - 1; i >= 0; i--) {
20
- const entry = this.entries[i];
21
- if (entry.kind === "tool" && entry.id === id && entry.result === null) {
22
- this.entries[i] = { ...entry, result, isError };
23
- this.version++;
24
- this.emit();
25
- return;
26
- }
27
- }
28
- }
29
- setAnswer(id, answer) {
30
- for (let i = this.entries.length - 1; i >= 0; i--) {
31
- const entry = this.entries[i];
32
- if (entry.kind === "question" && entry.id === id && entry.answer === null) {
33
- this.entries[i] = { ...entry, answer };
34
- this.version++;
35
- this.emit();
36
- return;
37
- }
38
- }
39
- }
40
- clear() {
41
- this.entries = [];
42
- this.version++;
43
- this.emit();
44
- }
45
- emit() {
46
- for (const listener of this.listeners)
47
- listener();
48
- }
49
- }
@@ -1,181 +0,0 @@
1
- import { Agent } from "./agent.js";
2
- import { Conversation } from "./conversation.js";
3
- import { LogStore } from "./logstore.js";
4
- export class Session {
5
- agent;
6
- mcp;
7
- commands;
8
- log = new LogStore();
9
- callbacks;
10
- streamingText = "";
11
- elapsed = 0;
12
- abortController = null;
13
- timer;
14
- startTime = 0;
15
- pendingQuestions = new Map();
16
- questionSeq = 0;
17
- getSnapshot = () => this.log.getSnapshot();
18
- subscribe = (listener) => this.log.subscribe(listener);
19
- get logEntries() {
20
- return this.log.all;
21
- }
22
- constructor(llm, systemPrompt, tools, commands, mcp) {
23
- const conversation = new Conversation(systemPrompt);
24
- this.agent = new Agent(llm, conversation, tools, (q, o) => this.ask(q, o));
25
- this.commands = commands;
26
- this.mcp = mcp;
27
- mcp.onError = (msg) => this.appendLog({ kind: "error", text: msg });
28
- for (const msg of mcp.flushErrors())
29
- this.appendLog({ kind: "error", text: msg });
30
- }
31
- dispose() {
32
- this.mcp.kill();
33
- }
34
- setCallbacks(cb) {
35
- this.callbacks = cb;
36
- }
37
- get contextTokens() {
38
- return this.agent.contextTokens;
39
- }
40
- appendLog(entry) {
41
- this.log.append(entry);
42
- }
43
- clearLog() {
44
- this.log.clear();
45
- }
46
- clear() {
47
- this.agent.clear();
48
- this.clearLog();
49
- }
50
- export() {
51
- return this.agent.export();
52
- }
53
- async compact() {
54
- const ctrl = new AbortController();
55
- this.abortController = ctrl;
56
- try {
57
- await this.agent.compact(ctrl.signal);
58
- return true;
59
- }
60
- catch (e) {
61
- if (ctrl.signal.aborted)
62
- return false;
63
- throw e;
64
- }
65
- finally {
66
- this.abortController = null;
67
- }
68
- }
69
- abort() {
70
- this.abortController?.abort();
71
- for (const id of this.pendingQuestions.keys()) {
72
- this.log.setAnswer(id, "");
73
- this.pendingQuestions.get(id)?.("");
74
- }
75
- this.pendingQuestions.clear();
76
- }
77
- ask(text, options) {
78
- const id = `q${++this.questionSeq}`;
79
- this.appendLog({ kind: "question", id, text, options, answer: null });
80
- return new Promise((resolve) => {
81
- this.pendingQuestions.set(id, resolve);
82
- });
83
- }
84
- submitAnswer(id, answer) {
85
- this.log.setAnswer(id, answer);
86
- const resolve = this.pendingQuestions.get(id);
87
- if (resolve) {
88
- this.pendingQuestions.delete(id);
89
- resolve(answer);
90
- }
91
- }
92
- get commandSchemas() {
93
- return this.commands.schemas();
94
- }
95
- isCommand(name) {
96
- return this.commands.exists(name);
97
- }
98
- async executeCommand(name, host) {
99
- await this.commands.execute(name, { session: this, mcp: this.mcp }, {
100
- exit: host.exit,
101
- info: (t) => this.appendLog({ kind: "system", text: t }),
102
- error: (t) => this.appendLog({ kind: "error", text: t }),
103
- thinking: (on) => host.setRunning(on),
104
- runSkill: (s) => this.startSkill(s),
105
- });
106
- }
107
- async startPrompt(text) {
108
- this.appendLog({ kind: "user", text });
109
- await this.run((signal) => this.agent.run(text, this.makeHandler(), signal));
110
- }
111
- async startSkill(skill) {
112
- this.appendLog({ kind: "skill", name: skill.name });
113
- await this.run((signal) => this.agent.runSkill(skill, this.makeHandler(), signal));
114
- }
115
- async run(runFn) {
116
- this.streamingText = "";
117
- this.elapsed = 0;
118
- this.startTime = Date.now();
119
- this.abortController = new AbortController();
120
- this.callbacks?.onElapsedChange?.(0);
121
- this.callbacks?.onUsageChange?.(0, 0);
122
- this.callbacks?.onRunStateChange?.(true);
123
- this.timer = setInterval(() => {
124
- this.elapsed = Math.floor((Date.now() - this.startTime) / 1000);
125
- this.callbacks?.onElapsedChange?.(this.elapsed);
126
- }, 1000);
127
- try {
128
- await runFn(this.abortController.signal);
129
- this.flushStreaming();
130
- }
131
- catch (e) {
132
- this.flushStreaming();
133
- this.appendLog({ kind: "error", text: e.message });
134
- }
135
- finally {
136
- clearInterval(this.timer);
137
- this.timer = undefined;
138
- this.abortController = null;
139
- this.callbacks?.onRunStateChange?.(false);
140
- }
141
- }
142
- makeHandler() {
143
- return (e) => {
144
- switch (e.type) {
145
- case "delta":
146
- this.streamingText += e.text;
147
- this.callbacks?.onStreaming?.(this.streamingText);
148
- break;
149
- case "tool_start":
150
- this.flushStreaming();
151
- this.appendLog({ kind: "tool", id: e.id, name: e.name, summary: e.summary, result: null });
152
- break;
153
- case "retry":
154
- this.streamingText = "";
155
- this.appendLog({ kind: "retry", attempt: e.attempt, max: e.max });
156
- break;
157
- case "tool_end":
158
- this.log.setResult(e.id, e.result, e.isError);
159
- break;
160
- case "error":
161
- this.flushStreaming();
162
- this.appendLog({ kind: "error", text: e.text });
163
- break;
164
- case "interrupted":
165
- this.flushStreaming();
166
- this.appendLog({ kind: "interrupted" });
167
- break;
168
- case "usage":
169
- this.callbacks?.onUsageChange?.(e.promptTokens, e.completionTokens);
170
- break;
171
- }
172
- };
173
- }
174
- flushStreaming() {
175
- if (this.streamingText) {
176
- this.appendLog({ kind: "assistant", text: this.streamingText });
177
- this.streamingText = "";
178
- this.callbacks?.onStreaming?.("");
179
- }
180
- }
181
- }
@@ -1,80 +0,0 @@
1
- import OpenAI, { APIConnectionError, APIError } from "openai";
2
- import { withRetry } from "../util/async.js";
3
- const MAX_RETRIES = 3;
4
- export class LLMClient {
5
- client;
6
- model;
7
- constructor(config) {
8
- this.client = new OpenAI({
9
- apiKey: config.apiKey,
10
- baseURL: config.baseUrl || undefined,
11
- maxRetries: 0,
12
- });
13
- this.model = config.model;
14
- }
15
- async chat(opts) {
16
- return withRetry(() => this.streamOnce(opts.messages, opts.tools, opts.onDelta, opts.onUsage, opts.signal), {
17
- retries: MAX_RETRIES,
18
- retryable: (e) => {
19
- if (e instanceof APIConnectionError)
20
- return true;
21
- if (e instanceof APIError && e.status)
22
- return e.status === 429 || e.status >= 500;
23
- return false;
24
- },
25
- backoff: (attempt) => 1000 * 2 ** attempt,
26
- onRetry: opts.onRetry,
27
- signal: opts.signal,
28
- });
29
- }
30
- async streamOnce(messages, tools, onDelta, onUsage, signal) {
31
- let content = "";
32
- const calls = new Map();
33
- const stream = await this.client.chat.completions.create({
34
- model: this.model,
35
- messages,
36
- tools,
37
- stream: true,
38
- stream_options: { include_usage: true },
39
- }, { signal });
40
- for await (const chunk of stream) {
41
- if (chunk.usage) {
42
- onUsage?.(chunk.usage.prompt_tokens ?? 0, chunk.usage.completion_tokens ?? 0);
43
- }
44
- const delta = chunk.choices[0]?.delta;
45
- if (!delta)
46
- continue;
47
- if (delta.content) {
48
- content += delta.content;
49
- onDelta?.(delta.content);
50
- }
51
- if (delta.tool_calls) {
52
- for (const tc of delta.tool_calls) {
53
- let acc = calls.get(tc.index);
54
- if (!acc) {
55
- acc = { id: tc.id ?? "", name: "", arguments: "" };
56
- calls.set(tc.index, acc);
57
- }
58
- if (tc.function?.name)
59
- acc.name += tc.function.name;
60
- if (tc.function?.arguments)
61
- acc.arguments += tc.function.arguments;
62
- }
63
- }
64
- }
65
- const message = {
66
- role: "assistant",
67
- content: content || null,
68
- };
69
- if (calls.size) {
70
- message.tool_calls = [...calls.entries()]
71
- .sort((a, b) => a[0] - b[0])
72
- .map(([, acc]) => ({
73
- id: acc.id,
74
- type: "function",
75
- function: { name: acc.name, arguments: acc.arguments },
76
- }));
77
- }
78
- return message;
79
- }
80
- }
package/dist/llm/types.js DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,81 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
5
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
- import { getPackageInfo } from "../util/package.js";
7
- function getClientInfo() {
8
- const pkginfo = getPackageInfo();
9
- return { name: pkginfo.name, version: pkginfo.version };
10
- }
11
- const STDERR_MAX_LINES = 20;
12
- export class MCPClient {
13
- name;
14
- client = new Client(getClientInfo(), { capabilities: {} });
15
- transport;
16
- connectReject;
17
- stderrBuf = [];
18
- constructor(name, config) {
19
- this.name = name;
20
- if ("command" in config) {
21
- const t = new StdioClientTransport({ ...config, stderr: "pipe" });
22
- this.transport = t;
23
- t.stderr?.on("data", (chunk) => {
24
- for (const line of chunk.toString("utf8").split(/\r?\n/)) {
25
- if (!line)
26
- continue;
27
- this.stderrBuf.push(line);
28
- if (this.stderrBuf.length > STDERR_MAX_LINES)
29
- this.stderrBuf.shift();
30
- }
31
- });
32
- }
33
- else {
34
- const opts = { requestInit: { headers: config.headers } };
35
- const url = new URL(config.url);
36
- this.transport =
37
- config.type === "sse"
38
- ? new SSEClientTransport(url, opts)
39
- : new StreamableHTTPClientTransport(url, opts);
40
- }
41
- }
42
- stderrTail() {
43
- return this.stderrBuf.join("\n");
44
- }
45
- async connect() {
46
- return new Promise((resolve, reject) => {
47
- this.connectReject = reject;
48
- this.client
49
- .connect(this.transport)
50
- .then(resolve, reject)
51
- .finally(() => {
52
- this.connectReject = undefined;
53
- });
54
- });
55
- }
56
- async listTools() {
57
- return this.client.listTools().then((r) => r.tools);
58
- }
59
- async callTool(name, args, signal) {
60
- return this.client.callTool({ name, arguments: args }, undefined, { signal });
61
- }
62
- kill() {
63
- this.connectReject?.(new Error("aborted"));
64
- this.connectReject = undefined;
65
- this.client.close().catch(() => { });
66
- if (this.transport instanceof StdioClientTransport) {
67
- const pid = this.transport.pid;
68
- if (!pid)
69
- return;
70
- try {
71
- if (process.platform === "win32") {
72
- spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true });
73
- }
74
- else {
75
- process.kill(pid, "SIGTERM");
76
- }
77
- }
78
- catch { }
79
- }
80
- }
81
- }
@@ -1,124 +0,0 @@
1
- import { MCPClient } from "./client.js";
2
- import { withTimeout } from "../util/async.js";
3
- const CONNECT_TIMEOUT = 30_000;
4
- function serverType(cfg) {
5
- return "command" in cfg ? "stdio" : cfg.type;
6
- }
7
- function fixError(text) {
8
- return text.startsWith("Error: ") ? text : `Error: ${text}`;
9
- }
10
- function extractContent(result) {
11
- const parts = [];
12
- for (const c of result.content) {
13
- switch (c.type) {
14
- case "text":
15
- parts.push(c.text);
16
- break;
17
- case "image":
18
- parts.push(`[image: ${c.mimeType}]`);
19
- break;
20
- case "audio":
21
- parts.push(`[audio: ${c.mimeType}]`);
22
- break;
23
- case "resource": {
24
- const r = c.resource;
25
- parts.push("text" in r ? r.text : `[resource: ${r.uri}]`);
26
- break;
27
- }
28
- default:
29
- parts.push(`[${c.type}]`);
30
- }
31
- }
32
- if (result.structuredContent) {
33
- parts.push(`<structured>${JSON.stringify(result.structuredContent)}</structured>`);
34
- }
35
- return parts.join("\n");
36
- }
37
- export class MCPServers {
38
- tools;
39
- servers = new Map();
40
- pending = new Set();
41
- disposed = false;
42
- errorBuffer = [];
43
- onError;
44
- constructor(tools) {
45
- this.tools = tools;
46
- }
47
- report(msg) {
48
- if (this.onError)
49
- this.onError(msg);
50
- else
51
- this.errorBuffer.push(msg);
52
- }
53
- flushErrors() {
54
- const buf = this.errorBuffer;
55
- this.errorBuffer = [];
56
- return buf;
57
- }
58
- async connect(mcpServers = {}) {
59
- await Promise.all(Object.entries(mcpServers).map(async ([name, cfg]) => {
60
- if (this.disposed)
61
- return;
62
- const type = serverType(cfg);
63
- if (cfg.enabled === false) {
64
- this.servers.set(name, { type, status: "disabled", tools: [] });
65
- return;
66
- }
67
- this.servers.set(name, { type, status: "pending", tools: [] });
68
- const client = new MCPClient(name, cfg);
69
- this.pending.add(client);
70
- try {
71
- await withTimeout(client.connect(), CONNECT_TIMEOUT);
72
- if (this.disposed) {
73
- client.kill();
74
- return;
75
- }
76
- const mcpTools = await withTimeout(client.listTools(), CONNECT_TIMEOUT);
77
- if (this.disposed) {
78
- client.kill();
79
- return;
80
- }
81
- this.servers.set(name, { type, status: "online", client, tools: mcpTools.map((t) => t.name) });
82
- for (const t of mcpTools)
83
- this.tools.register(this.adapt(name, client, t));
84
- }
85
- catch (e) {
86
- client.kill();
87
- if (!this.disposed) {
88
- this.servers.set(name, { type, status: "offline", tools: [] });
89
- const stderr = client.stderrTail();
90
- this.report(`MCP server "${name}" failed: ${e.message}${stderr ? `\n${stderr}` : ""}`);
91
- }
92
- }
93
- finally {
94
- this.pending.delete(client);
95
- }
96
- }));
97
- }
98
- adapt(server, client, tool) {
99
- return {
100
- name: `MCP__${server}__${tool.name}`,
101
- description: tool.description ?? `${server} ${tool.name}`,
102
- parameters: tool.inputSchema,
103
- async execute(args, ctx) {
104
- const result = await client.callTool(tool.name, args, ctx.signal);
105
- const text = extractContent(result);
106
- return result.isError
107
- ? { content: fixError(text), isError: true }
108
- : { content: text || "(no output)" };
109
- },
110
- };
111
- }
112
- list() {
113
- return [...this.servers.entries()].map(([name, s]) => ({ name, type: s.type, status: s.status, tools: s.tools }));
114
- }
115
- kill() {
116
- this.disposed = true;
117
- for (const { client } of this.servers.values())
118
- client?.kill();
119
- for (const client of this.pending)
120
- client.kill();
121
- this.servers.clear();
122
- this.pending.clear();
123
- }
124
- }
@@ -1,43 +0,0 @@
1
- import { existsSync, readdirSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { tryReadFileText } from "../util/fs.js";
4
- function parseSkillFile(skillName, skillFile) {
5
- const content = tryReadFileText(skillFile);
6
- if (!content)
7
- return undefined;
8
- const frontMatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
9
- const match = content.match(frontMatterRegex);
10
- let name = null;
11
- let description = "";
12
- let prompt = content;
13
- if (match) {
14
- const yamlBody = match[1];
15
- prompt = content.replace(match[0], "").trim();
16
- const nameMatch = yamlBody.match(/^name:\s*(.+)$/m);
17
- if (nameMatch)
18
- name = nameMatch[1].trim();
19
- const descMatch = yamlBody.match(/^description:\s*(.+)$/m);
20
- if (descMatch)
21
- description = descMatch[1].trim();
22
- }
23
- if (!name) {
24
- name = skillName;
25
- }
26
- return { name, description, prompt };
27
- }
28
- export function tryLoadSkills(path) {
29
- if (!existsSync(path))
30
- return undefined;
31
- const skills = [];
32
- for (const entry of readdirSync(path, { withFileTypes: true })) {
33
- if (!entry.isDirectory())
34
- continue;
35
- const skillFile = join(path, entry.name, "SKILL.md");
36
- if (!existsSync(skillFile))
37
- continue;
38
- const skill = parseSkillFile(entry.name, skillFile);
39
- if (skill && skill.prompt)
40
- skills.push(skill);
41
- }
42
- return skills.length > 0 ? skills : undefined;
43
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,24 +0,0 @@
1
- const DESCRIPTION = [
2
- "Ask the user a question and wait for their answer.",
3
- "Use when a decision belongs to the user: multiple reasonable approaches, an irreversible or consequential action, or an ambiguous request. Present choices via options rather than prose.",
4
- "options is an optional list of choices; the user may also type a custom answer.",
5
- "Returns the user's answer as text; an empty string means the user skipped the question.",
6
- ].join(" ");
7
- export const askUserTool = {
8
- name: "AskUser",
9
- description: DESCRIPTION,
10
- parameters: {
11
- type: "object",
12
- properties: {
13
- question: { type: "string", description: "The question to ask the user." },
14
- options: { type: "array", items: { type: "string" }, description: "Optional list of choices." },
15
- },
16
- required: ["question"],
17
- },
18
- async execute(args, ctx) {
19
- const question = args.question;
20
- const options = Array.isArray(args.options) ? args.options : [];
21
- return ctx.ask(question, options);
22
- },
23
- summaryArg: "question",
24
- };