assistme 0.3.2 → 0.3.4
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/dist/index.js +301 -212
- package/package.json +1 -1
- package/src/agent/processor.ts +63 -7
- package/src/agent/system-prompt.ts +6 -2
- package/src/browser/chrome-launcher.ts +6 -4
- package/src/browser/controller.ts +196 -134
- package/src/browser/types.ts +6 -0
- package/src/db/event.ts +32 -20
- package/src/mcp/agent-tools-server.ts +53 -40
- package/src/mcp/browser-server.ts +16 -33
- package/src/tools/browser.ts +1 -0
- package/src/tools/index.ts +0 -3
package/src/db/event.ts
CHANGED
|
@@ -2,6 +2,36 @@ import { callMcpHandler } from "./api-client.js";
|
|
|
2
2
|
import { log } from "../utils/logger.js";
|
|
3
3
|
import type { EventType } from "./types.js";
|
|
4
4
|
|
|
5
|
+
const MAX_EMIT_RETRIES = 2;
|
|
6
|
+
const EMIT_RETRY_DELAY_MS = 500;
|
|
7
|
+
|
|
8
|
+
async function emitWithRetry(
|
|
9
|
+
messageId: string,
|
|
10
|
+
eventType: EventType,
|
|
11
|
+
eventData: Record<string, unknown>,
|
|
12
|
+
seq: number
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
for (let attempt = 0; attempt <= MAX_EMIT_RETRIES; attempt++) {
|
|
15
|
+
try {
|
|
16
|
+
await callMcpHandler("event.emit", {
|
|
17
|
+
message_id: messageId,
|
|
18
|
+
event_type: eventType,
|
|
19
|
+
event_data: eventData,
|
|
20
|
+
seq,
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (attempt < MAX_EMIT_RETRIES) {
|
|
25
|
+
await new Promise((r) => setTimeout(r, EMIT_RETRY_DELAY_MS * (attempt + 1)));
|
|
26
|
+
} else {
|
|
27
|
+
log.warn(
|
|
28
|
+
`Failed to emit event after ${MAX_EMIT_RETRIES + 1} attempts: ${err instanceof Error ? err.message : err}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
5
35
|
/**
|
|
6
36
|
* Per-task event emitter. Each task gets its own sequence counter
|
|
7
37
|
* to avoid cross-task sequence number collisions.
|
|
@@ -13,16 +43,7 @@ export class TaskEventEmitter {
|
|
|
13
43
|
|
|
14
44
|
async emit(eventType: EventType, eventData: Record<string, unknown>): Promise<void> {
|
|
15
45
|
this.sequence++;
|
|
16
|
-
|
|
17
|
-
await callMcpHandler("event.emit", {
|
|
18
|
-
message_id: this.messageId,
|
|
19
|
-
event_type: eventType,
|
|
20
|
-
event_data: eventData,
|
|
21
|
-
seq: this.sequence,
|
|
22
|
-
});
|
|
23
|
-
} catch (err) {
|
|
24
|
-
log.warn(`Failed to emit event: ${err instanceof Error ? err.message : err}`);
|
|
25
|
-
}
|
|
46
|
+
await emitWithRetry(this.messageId, eventType, eventData, this.sequence);
|
|
26
47
|
}
|
|
27
48
|
}
|
|
28
49
|
|
|
@@ -39,14 +60,5 @@ export async function emitEvent(
|
|
|
39
60
|
eventData: Record<string, unknown>
|
|
40
61
|
): Promise<void> {
|
|
41
62
|
eventSequence++;
|
|
42
|
-
|
|
43
|
-
await callMcpHandler("event.emit", {
|
|
44
|
-
message_id: messageId,
|
|
45
|
-
event_type: eventType,
|
|
46
|
-
event_data: eventData,
|
|
47
|
-
seq: eventSequence,
|
|
48
|
-
});
|
|
49
|
-
} catch (err) {
|
|
50
|
-
log.warn(`Failed to emit event: ${err instanceof Error ? err.message : err}`);
|
|
51
|
-
}
|
|
63
|
+
await emitWithRetry(messageId, eventType, eventData, eventSequence);
|
|
52
64
|
}
|
|
@@ -25,10 +25,14 @@ export interface AgentToolsDeps {
|
|
|
25
25
|
skillManager: SkillManager;
|
|
26
26
|
taskId: string;
|
|
27
27
|
sessionId?: string;
|
|
28
|
+
/** Called when the agent starts waiting for user input (pauses task timeout). */
|
|
29
|
+
onUserWaitStart?: () => void;
|
|
30
|
+
/** Called when user input completes or times out (resumes task timeout). */
|
|
31
|
+
onUserWaitEnd?: () => void;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfigWithInstance {
|
|
31
|
-
const { memoryManager, skillManager, taskId, sessionId } = deps;
|
|
35
|
+
const { memoryManager, skillManager, taskId, sessionId, onUserWaitStart, onUserWaitEnd } = deps;
|
|
32
36
|
|
|
33
37
|
return createSdkMcpServer({
|
|
34
38
|
name: "assistme-agent",
|
|
@@ -582,56 +586,65 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
|
|
|
582
586
|
await setActionRequest(taskId, actionData);
|
|
583
587
|
log.info(`Ask user ${actionId}: "${args.question.slice(0, 80)}..."`);
|
|
584
588
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
emitEvent(taskId, "status_change", {
|
|
589
|
+
// Await event emissions to ensure they reach the DB before we start polling
|
|
590
|
+
await emitEvent(taskId, "user_action_request", actionData);
|
|
591
|
+
await emitEvent(taskId, "status_change", {
|
|
588
592
|
status: "waiting_for_user",
|
|
589
593
|
message: args.question,
|
|
590
|
-
})
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Pause the task wall-clock timeout while waiting for user
|
|
597
|
+
onUserWaitStart?.();
|
|
591
598
|
|
|
592
599
|
const startTime = Date.now();
|
|
593
600
|
const pollInterval = 2000;
|
|
594
601
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
602
|
+
try {
|
|
603
|
+
while (Date.now() - startTime < timeout) {
|
|
604
|
+
const response = await pollActionResponse(taskId);
|
|
605
|
+
if (response && (!response.action_id || response.action_id === actionId)) {
|
|
606
|
+
const actionKey = (response.action_key || "") as string;
|
|
607
|
+
const text = (response.text || "") as string;
|
|
608
|
+
const label = (response.label || actionKey || text) as string;
|
|
609
|
+
log.info(`User responded: "${label}"`);
|
|
610
|
+
return {
|
|
611
|
+
content: [
|
|
612
|
+
{
|
|
613
|
+
type: "text",
|
|
614
|
+
text: JSON.stringify({
|
|
615
|
+
status: "responded",
|
|
616
|
+
action_key: actionKey || "custom_input",
|
|
617
|
+
label,
|
|
618
|
+
text: text || label,
|
|
619
|
+
}),
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
616
626
|
}
|
|
617
627
|
|
|
618
|
-
|
|
628
|
+
log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
|
|
629
|
+
return {
|
|
630
|
+
content: [
|
|
631
|
+
{
|
|
632
|
+
type: "text",
|
|
633
|
+
text: JSON.stringify({
|
|
634
|
+
status: "timeout",
|
|
635
|
+
message:
|
|
636
|
+
"User did not respond within the timeout period. Continue the task with a reasonable default or skip the step that required user input.",
|
|
637
|
+
}),
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
};
|
|
641
|
+
} finally {
|
|
642
|
+
// Resume the task timeout regardless of outcome
|
|
643
|
+
onUserWaitEnd?.();
|
|
619
644
|
}
|
|
620
|
-
|
|
621
|
-
log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
|
|
622
|
-
return {
|
|
623
|
-
content: [
|
|
624
|
-
{
|
|
625
|
-
type: "text",
|
|
626
|
-
text: JSON.stringify({
|
|
627
|
-
status: "timeout",
|
|
628
|
-
message: "User did not respond within the timeout period.",
|
|
629
|
-
}),
|
|
630
|
-
},
|
|
631
|
-
],
|
|
632
|
-
};
|
|
633
645
|
} catch (err) {
|
|
634
646
|
log.error(`ask_user failed: ${err}`);
|
|
647
|
+
onUserWaitEnd?.();
|
|
635
648
|
return {
|
|
636
649
|
content: [
|
|
637
650
|
{
|
|
@@ -9,11 +9,15 @@ import { getLimiterForTool } from "../utils/rate-limiter.js";
|
|
|
9
9
|
|
|
10
10
|
// ── Helper ──────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
|
+
/** MCP content block — text or image. */
|
|
13
|
+
type ContentBlock =
|
|
14
|
+
| { type: "text"; text: string }
|
|
15
|
+
| { type: "image"; data: string; mimeType: string };
|
|
16
|
+
|
|
17
|
+
type ToolResult = { content: ContentBlock[] };
|
|
18
|
+
|
|
12
19
|
/** Wrap executeTool with rate limiting and text result. */
|
|
13
|
-
async function callTool(
|
|
14
|
-
name: string,
|
|
15
|
-
input: Record<string, unknown>
|
|
16
|
-
): Promise<{ content: Array<{ type: "text"; text: string }> }> {
|
|
20
|
+
async function callTool(name: string, input: Record<string, unknown>): Promise<ToolResult> {
|
|
17
21
|
const limiter = getLimiterForTool(name);
|
|
18
22
|
if (limiter) await limiter.acquire();
|
|
19
23
|
const result = await executeTool(name, input);
|
|
@@ -31,7 +35,6 @@ export const BROWSER_TOOL_NAMES = [
|
|
|
31
35
|
"browser_type",
|
|
32
36
|
"browser_press_key",
|
|
33
37
|
"browser_scroll",
|
|
34
|
-
"browser_get_elements",
|
|
35
38
|
"browser_select",
|
|
36
39
|
"browser_snapshot",
|
|
37
40
|
"browser_act",
|
|
@@ -69,19 +72,13 @@ export function createBrowserMcpServer(): McpSdkServerConfigWithInstance {
|
|
|
69
72
|
"browser_screenshot",
|
|
70
73
|
"Take a screenshot of the current browser page. Returns a base64-encoded PNG image.",
|
|
71
74
|
{},
|
|
72
|
-
async () => {
|
|
75
|
+
async (): Promise<ToolResult> => {
|
|
73
76
|
const limiter = getLimiterForTool("browser_screenshot");
|
|
74
77
|
if (limiter) await limiter.acquire();
|
|
75
78
|
const base64 = await executeTool("browser_screenshot", {});
|
|
76
79
|
if (base64.length > 100) {
|
|
77
80
|
return {
|
|
78
|
-
content: [
|
|
79
|
-
{
|
|
80
|
-
type: "image" as const,
|
|
81
|
-
data: base64,
|
|
82
|
-
mimeType: "image/png",
|
|
83
|
-
} as unknown as { type: "text"; text: string },
|
|
84
|
-
],
|
|
81
|
+
content: [{ type: "image", data: base64, mimeType: "image/png" }],
|
|
85
82
|
};
|
|
86
83
|
}
|
|
87
84
|
return { content: [{ type: "text", text: base64 }] };
|
|
@@ -114,12 +111,6 @@ export function createBrowserMcpServer(): McpSdkServerConfigWithInstance {
|
|
|
114
111
|
{ direction: z.string().describe("'down' or 'up'") },
|
|
115
112
|
async (args) => callTool("browser_scroll", args)
|
|
116
113
|
),
|
|
117
|
-
tool(
|
|
118
|
-
"browser_get_elements",
|
|
119
|
-
"Find all interactive elements (links, buttons, inputs) on the current page.",
|
|
120
|
-
{},
|
|
121
|
-
async () => callTool("browser_get_elements", {})
|
|
122
|
-
),
|
|
123
114
|
tool(
|
|
124
115
|
"browser_select",
|
|
125
116
|
"Select an option from a dropdown menu. Handles both native <select> elements and custom dropdowns (Material Design, React, Angular). Use this instead of manually clicking dropdown items.",
|
|
@@ -149,7 +140,7 @@ export function createBrowserMcpServer(): McpSdkServerConfigWithInstance {
|
|
|
149
140
|
"Overlay ref badges on the screenshot. Default false. Use true for simple pages where visual context helps."
|
|
150
141
|
),
|
|
151
142
|
},
|
|
152
|
-
async (args) => {
|
|
143
|
+
async (args): Promise<ToolResult> => {
|
|
153
144
|
const limiter = getLimiterForTool("browser_snapshot");
|
|
154
145
|
if (limiter) await limiter.acquire();
|
|
155
146
|
const result = await executeTool("browser_snapshot", args);
|
|
@@ -159,13 +150,9 @@ export function createBrowserMcpServer(): McpSdkServerConfigWithInstance {
|
|
|
159
150
|
const refTable = parts[0];
|
|
160
151
|
const imageData = parts[1] || "";
|
|
161
152
|
|
|
162
|
-
const content:
|
|
153
|
+
const content: ContentBlock[] = [];
|
|
163
154
|
if (imageData.length > 100) {
|
|
164
|
-
content.push({
|
|
165
|
-
type: "image" as const,
|
|
166
|
-
data: imageData,
|
|
167
|
-
mimeType: "image/png",
|
|
168
|
-
} as unknown as { type: "text"; text: string });
|
|
155
|
+
content.push({ type: "image", data: imageData, mimeType: "image/png" });
|
|
169
156
|
}
|
|
170
157
|
content.push({ type: "text", text: refTable });
|
|
171
158
|
|
|
@@ -197,7 +184,7 @@ export function createBrowserMcpServer(): McpSdkServerConfigWithInstance {
|
|
|
197
184
|
.optional()
|
|
198
185
|
.describe("Take screenshot after actions (default: false)"),
|
|
199
186
|
},
|
|
200
|
-
async (args) => {
|
|
187
|
+
async (args): Promise<ToolResult> => {
|
|
201
188
|
const limiter = getLimiterForTool("browser_act");
|
|
202
189
|
if (limiter) await limiter.acquire();
|
|
203
190
|
const result = await executeTool("browser_act", {
|
|
@@ -210,14 +197,10 @@ export function createBrowserMcpServer(): McpSdkServerConfigWithInstance {
|
|
|
210
197
|
const actionText = parts[0];
|
|
211
198
|
const screenshotData = parts[1] || "";
|
|
212
199
|
|
|
213
|
-
const content:
|
|
200
|
+
const content: ContentBlock[] = [];
|
|
214
201
|
content.push({ type: "text", text: actionText });
|
|
215
202
|
if (screenshotData.length > 100) {
|
|
216
|
-
content.push({
|
|
217
|
-
type: "image" as const,
|
|
218
|
-
data: screenshotData,
|
|
219
|
-
mimeType: "image/png",
|
|
220
|
-
} as unknown as { type: "text"; text: string });
|
|
203
|
+
content.push({ type: "image", data: screenshotData, mimeType: "image/png" });
|
|
221
204
|
}
|
|
222
205
|
|
|
223
206
|
return { content };
|
package/src/tools/browser.ts
CHANGED
package/src/tools/index.ts
CHANGED
|
@@ -169,9 +169,6 @@ export async function executeTool(name: string, input: Record<string, unknown>):
|
|
|
169
169
|
case "browser_scroll":
|
|
170
170
|
await ensureConnected(browser);
|
|
171
171
|
return (input.direction as string) === "up" ? browser.scrollUp() : browser.scrollDown();
|
|
172
|
-
case "browser_get_elements":
|
|
173
|
-
await ensureConnected(browser);
|
|
174
|
-
return browser.getInteractiveElements();
|
|
175
172
|
case "browser_select":
|
|
176
173
|
await ensureConnected(browser);
|
|
177
174
|
return browser.selectOption(input.selector as string, input.option as string);
|