@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.
@@ -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(/&amp;/g, '&')
24
+ .replace(/&lt;/g, '<')
25
+ .replace(/&gt;/g, '>')
26
+ .replace(/&quot;/g, '"')
27
+ .replace(/&#39;/g, "'")
28
+ .replace(/&nbsp;/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
+ });
@@ -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
  */