@vietor/easy-agent 0.1.1 → 0.3.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 +15 -13
- package/dist/cmds/builtin.js +10 -10
- package/dist/cmds/registry.js +3 -0
- package/dist/config.js +25 -3
- package/dist/core/agent.js +48 -30
- package/dist/core/conversation.js +82 -0
- package/dist/core/logstore.js +49 -0
- package/dist/core/session.js +173 -67
- package/dist/llm/client.js +12 -6
- package/dist/main.js +24 -24
- package/dist/mcp/client.js +44 -8
- package/dist/mcp/server.js +49 -10
- package/dist/tools/ask_user.js +24 -0
- package/dist/tools/file_edit.js +13 -5
- package/dist/tools/file_read.js +28 -3
- package/dist/tools/glob.js +3 -3
- package/dist/tools/grep.js +51 -14
- package/dist/tools/registry.js +4 -3
- package/dist/tools/shell.js +2 -2
- package/dist/tools/web_fetch.js +3 -2
- package/dist/tui/App.js +35 -108
- package/dist/tui/AppHeader.js +1 -1
- package/dist/tui/LogView.js +11 -7
- package/dist/tui/PromptOrCommandInput.js +14 -12
- package/dist/tui/QuestionView.js +45 -0
- package/dist/tui/Spinner.js +2 -2
- package/dist/util/package.js +5 -5
- package/dist/util/process.js +12 -2
- package/dist/util/ripgrep.js +3 -3
- package/package.json +9 -6
- package/dist/tui/LogStore.js +0 -35
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ Create `~/.easy-agent.json` in your home directory:
|
|
|
42
42
|
|
|
43
43
|
### Optional: MCP servers
|
|
44
44
|
|
|
45
|
-
Add an `mcpServers` map to expose external tools through the Model Context Protocol. Each entry
|
|
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 (SSE / Streamable HTTP):
|
|
46
46
|
|
|
47
47
|
```json
|
|
48
48
|
{
|
|
@@ -50,23 +50,24 @@ Add an `mcpServers` map to expose external tools through the Model Context Proto
|
|
|
50
50
|
"mcpServers": {
|
|
51
51
|
"chrome-devtools": {
|
|
52
52
|
"command": "npx",
|
|
53
|
-
"args": [
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
"args": ["chrome-devtools-mcp@latest", "--auto-connect", "--accept-insecure-certs"]
|
|
54
|
+
},
|
|
55
|
+
"remote-server": {
|
|
56
|
+
"type": "http",
|
|
57
|
+
"url": "https://mcp.example.com/mcp",
|
|
58
|
+
"headers": { "Authorization": "Bearer your-token" }
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
A stdio server omits `type` (or sets `"stdio"`); only `command` is required, `args` and `env` are optional. A remote server sets `type` to `"sse"` or `"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.
|
|
64
65
|
|
|
65
66
|
### Optional: Agent instructions
|
|
66
67
|
|
|
67
68
|
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:
|
|
68
69
|
|
|
69
|
-
1. **Global** — your home directory, applied to every
|
|
70
|
+
1. **Global** — your home directory, applied to every conversation. Checks `~/.agents/AGENTS.md` first, then `~/.claude/CLAUDE.md`; uses the first one found.
|
|
70
71
|
2. **Project** — your current working directory, applied per-project. Checks `./AGENTS.md` first, then `./CLAUDE.md`; uses the first one found.
|
|
71
72
|
|
|
72
73
|
Both global and project files are loaded and concatenated into the system prompt if they exist.
|
|
@@ -104,22 +105,23 @@ Launch the TUI:
|
|
|
104
105
|
easy-agent
|
|
105
106
|
```
|
|
106
107
|
|
|
107
|
-
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
|
|
108
|
+
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).
|
|
108
109
|
|
|
109
110
|
Built-in tools:
|
|
110
111
|
|
|
111
112
|
- **Shell** — run shell commands using this platform's native syntax.
|
|
112
|
-
- **FileRead** — read a file
|
|
113
|
+
- **FileRead** — read a file with line numbers; supports `offset`/`limit` for paging large files.
|
|
113
114
|
- **FileWrite** — create or fully overwrite a file.
|
|
114
|
-
- **FileEdit** — replace
|
|
115
|
+
- **FileEdit** — replace exact matches in a file; `replace_all` for every occurrence.
|
|
115
116
|
- **Glob** — list files, optionally filtered by a glob pattern.
|
|
116
|
-
- **Grep** — search file contents by regex.
|
|
117
|
+
- **Grep** — search file contents by regex with `glob`/`type` filters, context lines, case-insensitive, and `files_with_matches`/`count` output modes.
|
|
117
118
|
- **WebFetch** — fetch a URL as markdown or text.
|
|
119
|
+
- **AskUser** — ask the user a question and wait for their answer.
|
|
118
120
|
|
|
119
121
|
Slash commands:
|
|
120
122
|
|
|
121
123
|
- `/mcp` — list linked MCP servers, their status, and exposed tools.
|
|
122
124
|
- `/clear` — reset the conversation.
|
|
123
125
|
- `/compact` — compress the conversation into a summary to free context.
|
|
124
|
-
- `/export` — save the current
|
|
126
|
+
- `/export` — save the current conversation to `conversation-{timestamp}.jsonl`.
|
|
125
127
|
- `/quit` or `/exit` — leave the app.
|
package/dist/cmds/builtin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
export const exitCommand = {
|
|
3
3
|
name: "exit",
|
|
4
|
-
description: "Exit the
|
|
4
|
+
description: "Exit the conversation",
|
|
5
5
|
async execute(_ctx, host) {
|
|
6
6
|
host.exit();
|
|
7
7
|
},
|
|
@@ -9,9 +9,8 @@ export const exitCommand = {
|
|
|
9
9
|
export const clearCommand = {
|
|
10
10
|
name: "clear",
|
|
11
11
|
description: "Clear the conversation and log",
|
|
12
|
-
async execute(ctx
|
|
13
|
-
ctx.
|
|
14
|
-
host.clearLog();
|
|
12
|
+
async execute(ctx) {
|
|
13
|
+
ctx.session.clear();
|
|
15
14
|
},
|
|
16
15
|
};
|
|
17
16
|
export const mcpCommand = {
|
|
@@ -22,7 +21,7 @@ export const mcpCommand = {
|
|
|
22
21
|
const text = servers.length
|
|
23
22
|
? [
|
|
24
23
|
"MCP servers:",
|
|
25
|
-
...servers.map((s) => `❯ ${s.name} ⋅ ${s.status} ∶ ${s.tools.join(", ") || "(no tools)"}`),
|
|
24
|
+
...servers.map((s) => `❯ ${s.name} ⋅ ${s.type} ⋅ ${s.status} ∶ ${s.tools.join(", ") || "(no tools)"}`),
|
|
26
25
|
].join("\n")
|
|
27
26
|
: "No MCP servers linked.";
|
|
28
27
|
host.info(text);
|
|
@@ -34,8 +33,9 @@ export const compactCommand = {
|
|
|
34
33
|
async execute(ctx, host) {
|
|
35
34
|
host.thinking(true);
|
|
36
35
|
try {
|
|
37
|
-
await ctx.
|
|
38
|
-
|
|
36
|
+
const ok = await ctx.session.compact();
|
|
37
|
+
if (ok)
|
|
38
|
+
host.info("context compacted");
|
|
39
39
|
}
|
|
40
40
|
catch (e) {
|
|
41
41
|
host.error(e.message);
|
|
@@ -47,14 +47,14 @@ export const compactCommand = {
|
|
|
47
47
|
};
|
|
48
48
|
export const exportCommand = {
|
|
49
49
|
name: "export",
|
|
50
|
-
description: "Export the
|
|
50
|
+
description: "Export the conversation to a JSONL file",
|
|
51
51
|
async execute(ctx, host) {
|
|
52
52
|
try {
|
|
53
53
|
const d = new Date();
|
|
54
54
|
const pad = (n) => String(n).padStart(2, "0");
|
|
55
55
|
const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
56
|
-
const file = `
|
|
57
|
-
const lines = ctx.
|
|
56
|
+
const file = `conversation-${ts}.jsonl`;
|
|
57
|
+
const lines = ctx.session
|
|
58
58
|
.export()
|
|
59
59
|
.map((m) => JSON.stringify(m))
|
|
60
60
|
.join("\n");
|
package/dist/cmds/registry.js
CHANGED
package/dist/config.js
CHANGED
|
@@ -8,19 +8,41 @@ const LLMConfig = z.object({
|
|
|
8
8
|
apiKey: z.string(),
|
|
9
9
|
model: z.string(),
|
|
10
10
|
});
|
|
11
|
-
const
|
|
11
|
+
const StdioServerConfig = z.object({
|
|
12
|
+
type: z.literal("stdio").optional(),
|
|
12
13
|
command: z.string(),
|
|
13
14
|
args: z.array(z.string()).optional(),
|
|
14
15
|
env: z.record(z.string(), z.string()).optional(),
|
|
16
|
+
enabled: z.boolean().optional(),
|
|
15
17
|
});
|
|
18
|
+
const RemoteServerConfig = z.object({
|
|
19
|
+
type: z.enum(["sse", "http"]),
|
|
20
|
+
url: z.string().url(),
|
|
21
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
22
|
+
enabled: z.boolean().optional(),
|
|
23
|
+
});
|
|
24
|
+
const MCPServerConfig = z.union([StdioServerConfig, RemoteServerConfig]);
|
|
16
25
|
const Config = z.object({
|
|
17
26
|
llm: LLMConfig,
|
|
18
27
|
mcpServers: z.record(z.string(), MCPServerConfig).optional(),
|
|
19
28
|
});
|
|
20
29
|
export function loadConfig() {
|
|
21
30
|
const path = join(homedir(), CONFIG_FILE);
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = readFileSync(path, "utf-8");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error(`Config not found: create ~/${CONFIG_FILE} (see README for format).`);
|
|
37
|
+
}
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error(`Invalid JSON in ~/${CONFIG_FILE}.`);
|
|
44
|
+
}
|
|
45
|
+
const result = Config.safeParse(parsed);
|
|
24
46
|
if (!result.success) {
|
|
25
47
|
const issues = result.error.issues
|
|
26
48
|
.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
|
package/dist/core/agent.js
CHANGED
|
@@ -1,63 +1,70 @@
|
|
|
1
1
|
import { withAbort } from "../util/async.js";
|
|
2
2
|
const STALL_THRESHOLD = 3;
|
|
3
|
+
const MAX_TURNS = 50;
|
|
3
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:\".";
|
|
4
5
|
export class Agent {
|
|
5
6
|
llm;
|
|
6
|
-
|
|
7
|
+
conversation;
|
|
7
8
|
tools;
|
|
8
|
-
|
|
9
|
+
ask;
|
|
10
|
+
constructor(llm, conversation, tools, ask) {
|
|
9
11
|
this.llm = llm;
|
|
10
|
-
this.
|
|
12
|
+
this.conversation = conversation;
|
|
11
13
|
this.tools = tools;
|
|
14
|
+
this.ask = ask;
|
|
12
15
|
}
|
|
13
16
|
get contextTokens() {
|
|
14
|
-
return this.
|
|
17
|
+
return this.conversation.getEstimatedTokens();
|
|
15
18
|
}
|
|
16
19
|
clear() {
|
|
17
|
-
this.
|
|
20
|
+
this.conversation.clear();
|
|
18
21
|
}
|
|
19
22
|
export() {
|
|
20
|
-
return this.
|
|
23
|
+
return this.conversation.export();
|
|
21
24
|
}
|
|
22
|
-
async compact() {
|
|
23
|
-
const history = this.
|
|
25
|
+
async compact(signal) {
|
|
26
|
+
const history = this.conversation.toLLM().slice(1);
|
|
24
27
|
if (history.length === 0)
|
|
25
28
|
return;
|
|
26
29
|
const request = [
|
|
27
30
|
...history,
|
|
28
31
|
{ role: "user", content: COMPACT_PROMPT },
|
|
29
32
|
];
|
|
30
|
-
const msg = await this.llm.chat(request, []);
|
|
31
|
-
this.
|
|
33
|
+
const msg = await this.llm.chat({ messages: request, tools: [], signal });
|
|
34
|
+
this.conversation.compact(msg.content || "");
|
|
32
35
|
}
|
|
33
36
|
async run(userInput, onEvent, signal) {
|
|
34
|
-
this.
|
|
35
|
-
try {
|
|
36
|
-
this.session.add({ role: "user", content: userInput });
|
|
37
|
-
await this.loop(onEvent, signal);
|
|
38
|
-
}
|
|
39
|
-
finally {
|
|
40
|
-
this.session.removeCheckpoint();
|
|
41
|
-
}
|
|
37
|
+
await this.runTurn({ role: "user", content: userInput }, onEvent, signal);
|
|
42
38
|
}
|
|
43
39
|
async runSkill(skill, onEvent, signal) {
|
|
44
|
-
this.
|
|
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
45
|
try {
|
|
46
|
-
this.session.add({ role: "skill", name: skill.name, content: skill.prompt });
|
|
47
46
|
await this.loop(onEvent, signal);
|
|
48
47
|
}
|
|
49
48
|
finally {
|
|
50
|
-
this.
|
|
49
|
+
this.conversation.clearSnapshot();
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
async loop(onEvent, signal) {
|
|
54
53
|
await withAbort(async (aborted) => {
|
|
55
54
|
let lastSig = "";
|
|
56
55
|
let stall = 0;
|
|
56
|
+
let turns = 0;
|
|
57
57
|
while (true) {
|
|
58
58
|
let msg;
|
|
59
59
|
try {
|
|
60
|
-
msg = await this.llm.chat(
|
|
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
|
+
});
|
|
61
68
|
}
|
|
62
69
|
catch (e) {
|
|
63
70
|
if (aborted())
|
|
@@ -65,7 +72,7 @@ export class Agent {
|
|
|
65
72
|
onEvent?.({ type: "error", text: e.message });
|
|
66
73
|
return;
|
|
67
74
|
}
|
|
68
|
-
this.
|
|
75
|
+
this.conversation.add(msg);
|
|
69
76
|
if (!msg.tool_calls?.length)
|
|
70
77
|
return;
|
|
71
78
|
if (aborted())
|
|
@@ -79,7 +86,11 @@ export class Agent {
|
|
|
79
86
|
onEvent?.({ type: "error", text: "agent stalled: repeated identical tool calls" });
|
|
80
87
|
return;
|
|
81
88
|
}
|
|
82
|
-
|
|
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) => {
|
|
83
94
|
let args = {};
|
|
84
95
|
let argsError = "";
|
|
85
96
|
if (call.function.arguments) {
|
|
@@ -93,22 +104,29 @@ export class Agent {
|
|
|
93
104
|
const summary = this.tools.summarize(call.function.name, args);
|
|
94
105
|
onEvent?.({ type: "tool_start", id: call.id, name: call.function.name, summary });
|
|
95
106
|
if (aborted())
|
|
96
|
-
return;
|
|
107
|
+
return null;
|
|
108
|
+
const ctx = { signal, ask: this.ask };
|
|
97
109
|
const result = argsError
|
|
98
110
|
? { content: argsError, isError: true }
|
|
99
|
-
: await this.tools.execute(call.function.name, args);
|
|
111
|
+
: await this.tools.execute(call.function.name, args, ctx);
|
|
100
112
|
if (aborted())
|
|
101
|
-
return;
|
|
102
|
-
onEvent?.({ type: "tool_end", id: call.id,
|
|
103
|
-
|
|
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 };
|
|
104
116
|
}));
|
|
105
117
|
if (aborted())
|
|
106
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;
|
|
107
125
|
}
|
|
108
126
|
}, {
|
|
109
127
|
signal,
|
|
110
128
|
onAbort: () => {
|
|
111
|
-
this.
|
|
129
|
+
this.conversation.restoreFromSnapshot();
|
|
112
130
|
onEvent?.({ type: "interrupted" });
|
|
113
131
|
},
|
|
114
132
|
});
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
}
|