@yeaft/webchat-agent 0.1.442 → 0.1.444
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/connection/message-router.js +5 -1
- package/package.json +1 -1
- package/unify/config.js +18 -0
- package/unify/tools/agent.js +89 -0
- package/unify/tools/apply-patch.js +176 -0
- package/unify/tools/ask-user.js +62 -0
- package/unify/tools/bash.js +189 -0
- package/unify/tools/close-agent.js +58 -0
- package/unify/tools/file-edit.js +120 -0
- package/unify/tools/file-read.js +125 -0
- package/unify/tools/file-write.js +73 -0
- package/unify/tools/glob.js +143 -0
- package/unify/tools/grep.js +268 -0
- package/unify/tools/history-search.js +69 -0
- package/unify/tools/image-generation.js +97 -0
- package/unify/tools/index.js +92 -0
- package/unify/tools/js-repl.js +122 -0
- package/unify/tools/list-agents.js +56 -0
- package/unify/tools/list-dir.js +106 -0
- package/unify/tools/memory-read.js +91 -0
- package/unify/tools/memory-search.js +101 -0
- package/unify/tools/memory-write.js +114 -0
- package/unify/tools/notebook-edit.js +132 -0
- package/unify/tools/request-permissions.js +60 -0
- package/unify/tools/send-message.js +62 -0
- package/unify/tools/task-tools.js +358 -0
- package/unify/tools/tool-search.js +97 -0
- package/unify/tools/view-image.js +117 -0
- package/unify/tools/wait-agent.js +84 -0
- package/unify/tools/web-fetch.js +131 -0
- package/unify/tools/web-search.js +80 -0
- package/unify/tools/write-stdin.js +54 -0
- package/unify/web-bridge.js +27 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wait-agent.js — Wait for a sub-agent to complete and get its result.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineTool } from './types.js';
|
|
6
|
+
import { getAgentRegistry } from './agent.js';
|
|
7
|
+
|
|
8
|
+
export default defineTool({
|
|
9
|
+
name: 'WaitAgent',
|
|
10
|
+
description: `Wait for a sub-agent to complete its task and retrieve the result.
|
|
11
|
+
|
|
12
|
+
Returns the agent's final result or current status if still running.
|
|
13
|
+
Use after sending a task to an agent via SendMessage.`,
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
agent_id: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'The sub-agent ID to wait for',
|
|
20
|
+
},
|
|
21
|
+
timeout_ms: {
|
|
22
|
+
type: 'number',
|
|
23
|
+
description: 'Maximum time to wait in milliseconds (default: 30000)',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ['agent_id'],
|
|
27
|
+
},
|
|
28
|
+
modes: ['work'],
|
|
29
|
+
isConcurrencySafe: () => true,
|
|
30
|
+
isReadOnly: () => true,
|
|
31
|
+
async execute(input, ctx) {
|
|
32
|
+
const { agent_id, timeout_ms = 30000 } = input;
|
|
33
|
+
if (!agent_id) return JSON.stringify({ error: 'agent_id is required' });
|
|
34
|
+
|
|
35
|
+
const agents = getAgentRegistry();
|
|
36
|
+
const agent = agents.get(agent_id);
|
|
37
|
+
|
|
38
|
+
if (!agent) {
|
|
39
|
+
return JSON.stringify({ error: `Agent not found: ${agent_id}` });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If already completed, return result immediately
|
|
43
|
+
if (agent.status === 'completed' || agent.status === 'closed') {
|
|
44
|
+
return JSON.stringify({
|
|
45
|
+
agentId: agent_id,
|
|
46
|
+
name: agent.name,
|
|
47
|
+
status: agent.status,
|
|
48
|
+
result: agent.result,
|
|
49
|
+
messages: agent.messages.length,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Wait for completion with timeout
|
|
54
|
+
const deadline = Date.now() + timeout_ms;
|
|
55
|
+
while (Date.now() < deadline) {
|
|
56
|
+
if (agent.status === 'completed' || agent.status === 'closed') {
|
|
57
|
+
return JSON.stringify({
|
|
58
|
+
agentId: agent_id,
|
|
59
|
+
name: agent.name,
|
|
60
|
+
status: agent.status,
|
|
61
|
+
result: agent.result,
|
|
62
|
+
messages: agent.messages.length,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check abort signal
|
|
67
|
+
if (ctx?.signal?.aborted) {
|
|
68
|
+
return JSON.stringify({ error: 'Wait cancelled', agentId: agent_id });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Poll every 500ms
|
|
72
|
+
await new Promise(r => setTimeout(r, 500));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return JSON.stringify({
|
|
76
|
+
agentId: agent_id,
|
|
77
|
+
name: agent.name,
|
|
78
|
+
status: agent.status,
|
|
79
|
+
timedOut: true,
|
|
80
|
+
message: `Agent "${agent.name}" is still running after ${timeout_ms}ms`,
|
|
81
|
+
messages: agent.messages.length,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* web-fetch.js — Fetch web page content.
|
|
3
|
+
*
|
|
4
|
+
* Retrieves the content of a URL, converts HTML to readable text,
|
|
5
|
+
* and returns it for the LLM to process.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
|
|
10
|
+
/** Strip HTML tags and normalize whitespace for readability. */
|
|
11
|
+
function htmlToText(html) {
|
|
12
|
+
return html
|
|
13
|
+
// Remove script/style blocks
|
|
14
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
15
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
16
|
+
// Replace br/p/div/h tags with newlines
|
|
17
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
18
|
+
.replace(/<\/(p|div|h[1-6]|li|tr|blockquote)>/gi, '\n')
|
|
19
|
+
.replace(/<(p|div|h[1-6]|li|tr|blockquote)[^>]*>/gi, '\n')
|
|
20
|
+
// Strip remaining tags
|
|
21
|
+
.replace(/<[^>]+>/g, '')
|
|
22
|
+
// Decode common HTML entities
|
|
23
|
+
.replace(/&/g, '&')
|
|
24
|
+
.replace(/</g, '<')
|
|
25
|
+
.replace(/>/g, '>')
|
|
26
|
+
.replace(/"/g, '"')
|
|
27
|
+
.replace(/'/g, "'")
|
|
28
|
+
.replace(/ /g, ' ')
|
|
29
|
+
// Normalize whitespace
|
|
30
|
+
.replace(/[ \t]+/g, ' ')
|
|
31
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default defineTool({
|
|
36
|
+
name: 'WebFetch',
|
|
37
|
+
description: `Fetch and read the content of a web page.
|
|
38
|
+
|
|
39
|
+
Retrieves the URL content, strips HTML tags, and returns readable text.
|
|
40
|
+
Use this to read documentation, articles, or any web page.
|
|
41
|
+
|
|
42
|
+
Guidelines:
|
|
43
|
+
- Provide the full URL including protocol (https://)
|
|
44
|
+
- Large pages will be truncated — use the offset parameter for pagination
|
|
45
|
+
- For APIs, the raw response body is returned as-is
|
|
46
|
+
- Respects the abort signal for cancellation`,
|
|
47
|
+
parameters: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
url: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'The URL to fetch',
|
|
53
|
+
},
|
|
54
|
+
max_length: {
|
|
55
|
+
type: 'number',
|
|
56
|
+
description: 'Maximum content length in characters (default: 50000)',
|
|
57
|
+
},
|
|
58
|
+
raw: {
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
description: 'If true, return raw response without HTML stripping (for APIs)',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: ['url'],
|
|
64
|
+
},
|
|
65
|
+
modes: ['chat', 'work'],
|
|
66
|
+
isConcurrencySafe: () => true,
|
|
67
|
+
isReadOnly: () => true,
|
|
68
|
+
async execute(input, ctx) {
|
|
69
|
+
const { url, max_length = 50000, raw = false } = input;
|
|
70
|
+
if (!url) return JSON.stringify({ error: 'url is required' });
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Validate URL
|
|
74
|
+
let parsedUrl;
|
|
75
|
+
try {
|
|
76
|
+
parsedUrl = new URL(url);
|
|
77
|
+
} catch {
|
|
78
|
+
return JSON.stringify({ error: `Invalid URL: ${url}` });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Only allow http(s)
|
|
82
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
83
|
+
return JSON.stringify({ error: `Unsupported protocol: ${parsedUrl.protocol}` });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const response = await fetch(url, {
|
|
87
|
+
signal: ctx?.signal,
|
|
88
|
+
headers: {
|
|
89
|
+
'User-Agent': 'Yeaft/1.0 (compatible; bot)',
|
|
90
|
+
'Accept': 'text/html, application/json, text/plain, */*',
|
|
91
|
+
},
|
|
92
|
+
redirect: 'follow',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
return JSON.stringify({
|
|
97
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
98
|
+
url,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const contentType = response.headers.get('content-type') || '';
|
|
103
|
+
const body = await response.text();
|
|
104
|
+
|
|
105
|
+
let content;
|
|
106
|
+
if (raw || contentType.includes('json') || contentType.includes('text/plain')) {
|
|
107
|
+
content = body;
|
|
108
|
+
} else {
|
|
109
|
+
content = htmlToText(body);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Truncate if too long
|
|
113
|
+
const truncated = content.length > max_length;
|
|
114
|
+
if (truncated) {
|
|
115
|
+
content = content.slice(0, max_length);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return JSON.stringify({
|
|
119
|
+
url: response.url, // final URL after redirects
|
|
120
|
+
status: response.status,
|
|
121
|
+
contentType,
|
|
122
|
+
contentLength: content.length,
|
|
123
|
+
truncated,
|
|
124
|
+
content,
|
|
125
|
+
});
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err.name === 'AbortError') return JSON.stringify({ error: 'Fetch cancelled' });
|
|
128
|
+
return JSON.stringify({ error: `Fetch failed: ${err.message}`, url });
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* web-search.js — Web search tool.
|
|
3
|
+
*
|
|
4
|
+
* Delegates to an external search API or LLM-based web search.
|
|
5
|
+
* Supports configurable search providers via Yeaft config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
|
|
10
|
+
export default defineTool({
|
|
11
|
+
name: 'WebSearch',
|
|
12
|
+
description: `Search the web for current information.
|
|
13
|
+
|
|
14
|
+
Use this when you need up-to-date information that may not be in your training data.
|
|
15
|
+
Returns search results with titles, URLs, and snippets.
|
|
16
|
+
|
|
17
|
+
Guidelines:
|
|
18
|
+
- Use specific, targeted search queries
|
|
19
|
+
- Include the current year for time-sensitive queries
|
|
20
|
+
- Combine with WebFetch to read full page content from results`,
|
|
21
|
+
parameters: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
query: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'The search query',
|
|
27
|
+
},
|
|
28
|
+
limit: {
|
|
29
|
+
type: 'number',
|
|
30
|
+
description: 'Maximum number of results (default: 5)',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
required: ['query'],
|
|
34
|
+
},
|
|
35
|
+
modes: ['chat', 'work'],
|
|
36
|
+
isConcurrencySafe: () => true,
|
|
37
|
+
isReadOnly: () => true,
|
|
38
|
+
async execute(input, ctx) {
|
|
39
|
+
const { query, limit = 5 } = input;
|
|
40
|
+
if (!query) return JSON.stringify({ error: 'query is required' });
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Check if adapter supports web search natively (some LLM providers have built-in search)
|
|
44
|
+
const adapter = ctx?.adapter;
|
|
45
|
+
if (adapter && typeof adapter.webSearch === 'function') {
|
|
46
|
+
const results = await adapter.webSearch(query, limit);
|
|
47
|
+
return JSON.stringify(results, null, 2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check config for search API endpoint
|
|
51
|
+
const searchUrl = ctx?.config?.searchApiUrl;
|
|
52
|
+
if (searchUrl) {
|
|
53
|
+
const url = new URL(searchUrl);
|
|
54
|
+
url.searchParams.set('q', query);
|
|
55
|
+
url.searchParams.set('limit', String(limit));
|
|
56
|
+
|
|
57
|
+
const response = await fetch(url.toString(), {
|
|
58
|
+
signal: ctx?.signal,
|
|
59
|
+
headers: { 'User-Agent': 'Yeaft/1.0' },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
return JSON.stringify({ error: `Search API returned ${response.status}: ${response.statusText}` });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
return JSON.stringify(data, null, 2);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Fallback: no search provider configured
|
|
71
|
+
return JSON.stringify({
|
|
72
|
+
error: 'No web search provider configured.',
|
|
73
|
+
hint: 'Configure searchApiUrl in ~/.yeaft/config.json or use an LLM provider with built-in search.',
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err.name === 'AbortError') return JSON.stringify({ error: 'Search cancelled' });
|
|
77
|
+
return JSON.stringify({ error: `Web search failed: ${err.message}` });
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* write-stdin.js — Write data to a running process's stdin.
|
|
3
|
+
*
|
|
4
|
+
* Used in conjunction with Bash for processes that need interactive input.
|
|
5
|
+
* Currently returns a guidance message since Bash tool uses 'ignore' for stdin.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTool } from './types.js';
|
|
9
|
+
|
|
10
|
+
export default defineTool({
|
|
11
|
+
name: 'WriteStdin',
|
|
12
|
+
description: `Write data to a running process's standard input.
|
|
13
|
+
|
|
14
|
+
This tool is intended for sending input to interactive processes.
|
|
15
|
+
Since the Bash tool runs commands non-interactively, this is primarily
|
|
16
|
+
useful with the terminal system or long-running processes.
|
|
17
|
+
|
|
18
|
+
Note: For most use cases, pipe input via Bash: echo "input" | command`,
|
|
19
|
+
parameters: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
process_id: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Process identifier or terminal ID',
|
|
25
|
+
},
|
|
26
|
+
data: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Data to write to stdin',
|
|
29
|
+
},
|
|
30
|
+
newline: {
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
description: 'Append newline after data (default: true)',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['data'],
|
|
36
|
+
},
|
|
37
|
+
modes: ['work'],
|
|
38
|
+
isConcurrencySafe: () => false,
|
|
39
|
+
isReadOnly: () => false,
|
|
40
|
+
async execute(input, ctx) {
|
|
41
|
+
const { process_id, data, newline = true } = input;
|
|
42
|
+
if (!data && data !== '') return JSON.stringify({ error: 'data is required' });
|
|
43
|
+
|
|
44
|
+
// The Bash tool uses 'ignore' for stdin, so direct stdin writing
|
|
45
|
+
// is only possible through the terminal system.
|
|
46
|
+
// For most interactive needs, recommend using pipe syntax.
|
|
47
|
+
return JSON.stringify({
|
|
48
|
+
hint: 'The Bash tool does not support interactive stdin. Use pipe syntax instead:',
|
|
49
|
+
example: `echo "${data}" | your_command`,
|
|
50
|
+
alternativeBash: `printf '%s\\n' '${data.replace(/'/g, "'\\''")}' | your_command`,
|
|
51
|
+
message: 'For interactive processes, use the terminal system (not the AI tool system).',
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
});
|
package/unify/web-bridge.js
CHANGED
|
@@ -103,6 +103,7 @@ export async function handleUnifyChat(msg) {
|
|
|
103
103
|
type: 'session_ready',
|
|
104
104
|
conversationId: unifyConversationId,
|
|
105
105
|
model: session.config.model,
|
|
106
|
+
availableModels: session.config.availableModels || [],
|
|
106
107
|
skills: session.status.skills,
|
|
107
108
|
mcpServers: session.status.mcpServers,
|
|
108
109
|
tools: session.status.tools,
|
|
@@ -385,6 +386,32 @@ export function handleUnifyModeSwitch(msg) {
|
|
|
385
386
|
}
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Handle model switch from the web UI.
|
|
391
|
+
* Updates Engine's config so the next query uses the new model.
|
|
392
|
+
* @param {{ model: string }} msg
|
|
393
|
+
*/
|
|
394
|
+
export function handleUnifyModelSwitch(msg) {
|
|
395
|
+
if (!session || !msg.model) return;
|
|
396
|
+
|
|
397
|
+
// Validate: model must be in availableModels list
|
|
398
|
+
const available = session.config.availableModels || [];
|
|
399
|
+
const found = available.some(m => m.id === msg.model);
|
|
400
|
+
if (!found) {
|
|
401
|
+
console.warn(`[Unify] model switch rejected — "${msg.model}" not in availableModels`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Update Engine's model for subsequent queries
|
|
406
|
+
session.config.model = msg.model;
|
|
407
|
+
|
|
408
|
+
// Confirm switch to frontend
|
|
409
|
+
sendUnifyEvent({
|
|
410
|
+
type: 'model_switched',
|
|
411
|
+
model: msg.model,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
388
415
|
/**
|
|
389
416
|
* Reset Unify session (for clear messages).
|
|
390
417
|
*/
|