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/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
- try {
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
- try {
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
- emitEvent(taskId, "user_action_request", actionData).catch(() => {});
586
- // Emit waiting_for_user status so the web UI can show a waiting indicator
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
- }).catch(() => {});
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
- while (Date.now() - startTime < timeout) {
596
- const response = await pollActionResponse(taskId);
597
- if (response && (!response.action_id || response.action_id === actionId)) {
598
- // Response can be either an option click or free-text input
599
- const actionKey = (response.action_key || "") as string;
600
- const text = (response.text || "") as string;
601
- const label = (response.label || actionKey || text) as string;
602
- log.info(`User responded: "${label}"`);
603
- return {
604
- content: [
605
- {
606
- type: "text",
607
- text: JSON.stringify({
608
- status: "responded",
609
- action_key: actionKey || "custom_input",
610
- label,
611
- text: text || label,
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
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
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: Array<{ type: "text"; text: string }> = [];
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: Array<{ type: "text"; text: string }> = [];
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 };
@@ -14,6 +14,7 @@ export type {
14
14
  SnapshotResult,
15
15
  ActionSpec,
16
16
  ActionResult,
17
+ RefActionResult,
17
18
  AutoLaunchResult,
18
19
  } from "../browser/types.js";
19
20
 
@@ -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);