yiyan-browser-agent 1.0.1 → 1.0.3

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/src/agent.js ADDED
@@ -0,0 +1,257 @@
1
+ // src/agent.js — The core agent loop that ties everything together
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { execSync } = require('child_process');
7
+ const config = require('./config');
8
+ const logger = require('./logger');
9
+ const YiyanBrowser = require('./browser');
10
+ const { executeTool } = require('./tools');
11
+ const { parseResponse,
12
+ formatToolResult } = require('./parser');
13
+ const { ConversationManager } = require('./prompt');
14
+
15
+ // ─────────────────────────────────────────────
16
+ // Agent class
17
+ // ─────────────────────────────────────────────
18
+
19
+ class YiyanAgent {
20
+ constructor(options = {}) {
21
+ this.browser = new YiyanBrowser();
22
+ this.conversation = new ConversationManager();
23
+ this.options = options;
24
+ this._running = false;
25
+ }
26
+
27
+ // ── Public API ──────────────────────────────────────────────────────────────
28
+
29
+ /** Boot the browser and load Yiyan */
30
+ async init() {
31
+ await this.browser.launch();
32
+ await this.browser.newChat();
33
+ }
34
+
35
+ /** Shut down cleanly */
36
+ async shutdown() {
37
+ await this.browser.close();
38
+ }
39
+
40
+ /**
41
+ * Run a task to completion.
42
+ * Returns JSON object with question, answer, duration.
43
+ */
44
+ async run(task) {
45
+ this._running = true;
46
+ const maxIter = config.MAX_ITERATIONS;
47
+ const startTime = Date.now();
48
+
49
+ // ── Send task directly ───────────────────────────────────
50
+ logger.info('Sending task to Yiyan...');
51
+ await this.browser.sendMessage(task);
52
+
53
+ // ── Agent loop ──────────────────────────────────────────────────────
54
+ let finalAnswer = '';
55
+ for (let iter = 1; iter <= maxIter; iter++) {
56
+ logger.iteration(iter, maxIter);
57
+
58
+ // Wait for response from Yiyan
59
+ const rawResponse = await this.browser.waitForResponse();
60
+
61
+ if (!rawResponse || rawResponse.trim().length === 0) {
62
+ logger.warn('Empty response received — retrying...');
63
+ await this.browser.sendMessage('Please continue.');
64
+ continue;
65
+ }
66
+
67
+ if (config.DEBUG) {
68
+ logger.dim(`--- Raw response (${rawResponse.length} chars) ---`);
69
+ logger.dim(rawResponse.slice(0, 400));
70
+ }
71
+
72
+ this.conversation.addAssistantMessage(rawResponse);
73
+ const parsed = parseResponse(rawResponse);
74
+
75
+ // ── Tool call ────────────────────────────────────────────────────
76
+ if (parsed.type === 'tool_call') {
77
+ logger.toolCall(parsed.name, parsed.args);
78
+ let result, isError = false;
79
+ try {
80
+ result = await executeTool(parsed.name, parsed.args);
81
+ logger.toolResult(result);
82
+ } catch (err) {
83
+ result = `Error: ${err.message}`;
84
+ isError = true;
85
+ logger.toolResult(result, true);
86
+ }
87
+ const feedbackMsg = this.conversation.addToolResult(parsed.name, result, isError);
88
+ await this.browser.sendMessage(feedbackMsg);
89
+ continue;
90
+ }
91
+
92
+ // ── Parse error ──────────────────────────────────────────────────
93
+ if (parsed.type === 'error') {
94
+ logger.warn(`Parse error: ${parsed.message}`);
95
+ const recovery = this.conversation.addToolResult('SYSTEM', `Parse error: ${parsed.message}`, true);
96
+ await this.browser.sendMessage(recovery);
97
+ continue;
98
+ }
99
+
100
+ // ── Final response ───────────────────────────────────────────────
101
+ if (parsed.type === 'final') {
102
+ finalAnswer = parsed.content;
103
+ if (this.options.saveLog) {
104
+ await this._saveConversationLog(task, parsed.content);
105
+ }
106
+ this._running = false;
107
+ break;
108
+ }
109
+ }
110
+
111
+ const duration = Date.now() - startTime;
112
+ let status = 'success';
113
+ if (!finalAnswer) {
114
+ finalAnswer = `Reached maximum iterations (${maxIter}).`;
115
+ status = 'incomplete';
116
+ }
117
+ this._running = false;
118
+
119
+ // Return JSON
120
+ return {
121
+ question: task,
122
+ answer: finalAnswer,
123
+ duration: duration,
124
+ status: status
125
+ };
126
+ }
127
+
128
+ // ── Interactive (REPL) Mode ────────────────────────────────────────────────
129
+
130
+ /**
131
+ * Run the agent in interactive mode — keeps the browser open
132
+ * and outputs JSON format.
133
+ */
134
+ async runInteractive() {
135
+ const readline = require('readline');
136
+
137
+ const rl = readline.createInterface({
138
+ input : process.stdin,
139
+ output : process.stdout,
140
+ terminal : true,
141
+ });
142
+
143
+ const ask = () => new Promise(resolve => rl.question('', resolve));
144
+
145
+ while (true) {
146
+ let task;
147
+ try {
148
+ task = (await ask()).trim();
149
+ } catch {
150
+ break;
151
+ }
152
+
153
+ if (!task) continue;
154
+
155
+ if (['exit', 'quit', 'q'].includes(task.toLowerCase())) {
156
+ break;
157
+ }
158
+
159
+ if (task.toLowerCase() === 'new') {
160
+ await this.browser.newChat();
161
+ this.conversation = new ConversationManager();
162
+ continue;
163
+ }
164
+
165
+ this.conversation = new ConversationManager();
166
+
167
+ try {
168
+ await this.browser.newChat();
169
+ const result = await this.run(task);
170
+ // Output JSON to stdout
171
+ console.log(JSON.stringify(result, null, 2));
172
+ } catch (err) {
173
+ console.log(JSON.stringify({
174
+ question: task,
175
+ answer: `Error: ${err.message}`,
176
+ duration: 0,
177
+ status: 'error'
178
+ }, null, 2));
179
+ }
180
+ }
181
+
182
+ rl.close();
183
+ }
184
+
185
+ // ── Helpers ────────────────────────────────────────────────────────────────
186
+
187
+ _getWorkingDirListing() {
188
+ try {
189
+ // Use Node.js fs for cross-platform compatibility (Windows compatible)
190
+ const fs = require('fs');
191
+ const pathModule = require('path');
192
+ const cwd = config.WORKING_DIR;
193
+
194
+ const excludeDirs = ['node_modules', '.git', 'dist', '.next', 'build', '__pycache__', '.idea', '.vscode'];
195
+ const excludeFiles = ['.lock', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
196
+
197
+ let results = [];
198
+
199
+ function walk(dir, depth) {
200
+ if (depth > 3) return;
201
+ try {
202
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
203
+ for (const entry of entries) {
204
+ const fullPath = pathModule.join(dir, entry.name);
205
+ const relativePath = pathModule.relative(cwd, fullPath);
206
+
207
+ if (entry.isDirectory()) {
208
+ if (excludeDirs.includes(entry.name)) continue;
209
+ results.push(relativePath);
210
+ walk(fullPath, depth + 1);
211
+ } else if (entry.isFile()) {
212
+ if (excludeFiles.some(ext => entry.name.endsWith(ext))) continue;
213
+ results.push(relativePath);
214
+ }
215
+ }
216
+ } catch {}
217
+ }
218
+
219
+ walk(cwd, 1);
220
+ return results.slice(0, 80).join('\n') || '(empty directory)';
221
+ } catch {
222
+ return '(could not read directory)';
223
+ }
224
+ }
225
+
226
+ async _saveConversationLog(task, finalResponse) {
227
+ try {
228
+ const logsDir = path.join(os.homedir(), '.yiyan-agent', 'logs');
229
+ fs.mkdirSync(logsDir, { recursive: true });
230
+
231
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
232
+ const logFile = path.join(logsDir, `session-${ts}.txt`);
233
+ const content = [
234
+ `Yiyan Agent — Session Log`,
235
+ `Date: ${new Date().toISOString()}`,
236
+ `Task: ${task}`,
237
+ `Working Dir: ${config.WORKING_DIR}`,
238
+ '═'.repeat(60),
239
+ this.conversation.exportLog(),
240
+ '',
241
+ '═'.repeat(60),
242
+ 'FINAL RESPONSE:',
243
+ finalResponse,
244
+ ].join('\n');
245
+
246
+ fs.writeFileSync(logFile, content, 'utf8');
247
+ logger.dim(`Conversation saved: ${logFile}`);
248
+ } catch (err) {
249
+ logger.warn(`Could not save log: ${err.message}`);
250
+ }
251
+ }
252
+ }
253
+
254
+ // Pull os into scope for the log save helper
255
+ const os = require('os');
256
+
257
+ module.exports = YiyanAgent;