assistme 0.3.3 → 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 +113 -55
- package/package.json +1 -1
- package/src/agent/processor.ts +55 -6
- package/src/agent/system-prompt.ts +5 -1
- package/src/db/event.ts +32 -20
- package/src/mcp/agent-tools-server.ts +53 -40
package/dist/index.js
CHANGED
|
@@ -171,22 +171,36 @@ async function getConversationHistory(conversationId, excludeMessageId, limit =
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
// src/db/event.ts
|
|
174
|
+
var MAX_EMIT_RETRIES = 2;
|
|
175
|
+
var EMIT_RETRY_DELAY_MS = 500;
|
|
176
|
+
async function emitWithRetry(messageId, eventType, eventData, seq) {
|
|
177
|
+
for (let attempt = 0; attempt <= MAX_EMIT_RETRIES; attempt++) {
|
|
178
|
+
try {
|
|
179
|
+
await callMcpHandler("event.emit", {
|
|
180
|
+
message_id: messageId,
|
|
181
|
+
event_type: eventType,
|
|
182
|
+
event_data: eventData,
|
|
183
|
+
seq
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if (attempt < MAX_EMIT_RETRIES) {
|
|
188
|
+
await new Promise((r) => setTimeout(r, EMIT_RETRY_DELAY_MS * (attempt + 1)));
|
|
189
|
+
} else {
|
|
190
|
+
log.warn(
|
|
191
|
+
`Failed to emit event after ${MAX_EMIT_RETRIES + 1} attempts: ${err instanceof Error ? err.message : err}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
174
197
|
var eventSequence = 0;
|
|
175
198
|
function resetEventSequence() {
|
|
176
199
|
eventSequence = 0;
|
|
177
200
|
}
|
|
178
201
|
async function emitEvent(messageId, eventType, eventData) {
|
|
179
202
|
eventSequence++;
|
|
180
|
-
|
|
181
|
-
await callMcpHandler("event.emit", {
|
|
182
|
-
message_id: messageId,
|
|
183
|
-
event_type: eventType,
|
|
184
|
-
event_data: eventData,
|
|
185
|
-
seq: eventSequence
|
|
186
|
-
});
|
|
187
|
-
} catch (err) {
|
|
188
|
-
log.warn(`Failed to emit event: ${err instanceof Error ? err.message : err}`);
|
|
189
|
-
}
|
|
203
|
+
await emitWithRetry(messageId, eventType, eventData, eventSequence);
|
|
190
204
|
}
|
|
191
205
|
|
|
192
206
|
// src/db/action.ts
|
|
@@ -4183,7 +4197,7 @@ function getCredentialStore() {
|
|
|
4183
4197
|
|
|
4184
4198
|
// src/mcp/agent-tools-server.ts
|
|
4185
4199
|
function createAgentToolsServer(deps) {
|
|
4186
|
-
const { memoryManager, skillManager, taskId, sessionId } = deps;
|
|
4200
|
+
const { memoryManager, skillManager, taskId, sessionId, onUserWaitStart, onUserWaitEnd } = deps;
|
|
4187
4201
|
return createSdkMcpServer2({
|
|
4188
4202
|
name: "assistme-agent",
|
|
4189
4203
|
version: "1.0.0",
|
|
@@ -4688,52 +4702,56 @@ Use \`ask_user\` to request these from the user, or create them yourself (e.g. r
|
|
|
4688
4702
|
try {
|
|
4689
4703
|
await setActionRequest(taskId, actionData);
|
|
4690
4704
|
log.info(`Ask user ${actionId}: "${args.question.slice(0, 80)}..."`);
|
|
4691
|
-
emitEvent(taskId, "user_action_request", actionData)
|
|
4692
|
-
|
|
4693
|
-
emitEvent(taskId, "status_change", {
|
|
4705
|
+
await emitEvent(taskId, "user_action_request", actionData);
|
|
4706
|
+
await emitEvent(taskId, "status_change", {
|
|
4694
4707
|
status: "waiting_for_user",
|
|
4695
4708
|
message: args.question
|
|
4696
|
-
}).catch(() => {
|
|
4697
4709
|
});
|
|
4710
|
+
onUserWaitStart?.();
|
|
4698
4711
|
const startTime = Date.now();
|
|
4699
4712
|
const pollInterval = 2e3;
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4713
|
+
try {
|
|
4714
|
+
while (Date.now() - startTime < timeout) {
|
|
4715
|
+
const response = await pollActionResponse(taskId);
|
|
4716
|
+
if (response && (!response.action_id || response.action_id === actionId)) {
|
|
4717
|
+
const actionKey = response.action_key || "";
|
|
4718
|
+
const text = response.text || "";
|
|
4719
|
+
const label = response.label || actionKey || text;
|
|
4720
|
+
log.info(`User responded: "${label}"`);
|
|
4721
|
+
return {
|
|
4722
|
+
content: [
|
|
4723
|
+
{
|
|
4724
|
+
type: "text",
|
|
4725
|
+
text: JSON.stringify({
|
|
4726
|
+
status: "responded",
|
|
4727
|
+
action_key: actionKey || "custom_input",
|
|
4728
|
+
label,
|
|
4729
|
+
text: text || label
|
|
4730
|
+
})
|
|
4731
|
+
}
|
|
4732
|
+
]
|
|
4733
|
+
};
|
|
4734
|
+
}
|
|
4735
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
4720
4736
|
}
|
|
4721
|
-
|
|
4737
|
+
log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
|
|
4738
|
+
return {
|
|
4739
|
+
content: [
|
|
4740
|
+
{
|
|
4741
|
+
type: "text",
|
|
4742
|
+
text: JSON.stringify({
|
|
4743
|
+
status: "timeout",
|
|
4744
|
+
message: "User did not respond within the timeout period. Continue the task with a reasonable default or skip the step that required user input."
|
|
4745
|
+
})
|
|
4746
|
+
}
|
|
4747
|
+
]
|
|
4748
|
+
};
|
|
4749
|
+
} finally {
|
|
4750
|
+
onUserWaitEnd?.();
|
|
4722
4751
|
}
|
|
4723
|
-
log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
|
|
4724
|
-
return {
|
|
4725
|
-
content: [
|
|
4726
|
-
{
|
|
4727
|
-
type: "text",
|
|
4728
|
-
text: JSON.stringify({
|
|
4729
|
-
status: "timeout",
|
|
4730
|
-
message: "User did not respond within the timeout period."
|
|
4731
|
-
})
|
|
4732
|
-
}
|
|
4733
|
-
]
|
|
4734
|
-
};
|
|
4735
4752
|
} catch (err) {
|
|
4736
4753
|
log.error(`ask_user failed: ${err}`);
|
|
4754
|
+
onUserWaitEnd?.();
|
|
4737
4755
|
return {
|
|
4738
4756
|
content: [
|
|
4739
4757
|
{
|
|
@@ -5167,12 +5185,16 @@ Available capabilities:
|
|
|
5167
5185
|
- Bash tool for shell commands
|
|
5168
5186
|
- Glob and Grep for file search
|
|
5169
5187
|
|
|
5170
|
-
3. MEMORY:
|
|
5188
|
+
3. MEMORY & CREDENTIALS:
|
|
5171
5189
|
- You can remember things about the user using memory_store
|
|
5172
5190
|
- Use this when you learn preferences, important facts, or standing instructions
|
|
5173
5191
|
- Your stored memories persist across conversations
|
|
5174
5192
|
- PROACTIVELY use memory_store during tasks when you discover user preferences, habits, or important context
|
|
5175
5193
|
- Before completing a task, consider if anything learned should be remembered for future conversations
|
|
5194
|
+
- CRITICAL \u2014 Credential Storage: When you create, register, or receive any account credentials (username, password, API keys, tokens), you MUST use credential_set to save them locally. NEVER use memory_store for credentials \u2014 memory_store is for preferences and facts, credential_set is for secrets. Examples:
|
|
5195
|
+
* After registering a new email/account \u2192 credential_set with type "login" and data { "username": "...", "password": "...", "email": "..." }
|
|
5196
|
+
* After generating an API key \u2192 credential_set with type "api_key" and data { "api_key": "..." }
|
|
5197
|
+
* Credentials saved via credential_set are encrypted on disk and viewable in the desktop app's Credentials panel
|
|
5176
5198
|
|
|
5177
5199
|
4. SKILL-AWARE EXECUTION (CRITICAL \u2014 follow this for EVERY task):
|
|
5178
5200
|
Step A \u2014 Search: Before executing ANY task, check if an existing skill matches (use skill_invoke or skill_search).
|
|
@@ -5252,6 +5274,42 @@ CRITICAL \u2014 Ask before you guess:
|
|
|
5252
5274
|
Workspace path: {workspace_path}`;
|
|
5253
5275
|
|
|
5254
5276
|
// src/agent/processor.ts
|
|
5277
|
+
var TaskTimeout = class {
|
|
5278
|
+
constructor(abortController, timeoutMs) {
|
|
5279
|
+
this.abortController = abortController;
|
|
5280
|
+
this.remainingMs = timeoutMs;
|
|
5281
|
+
this.resumedAt = Date.now();
|
|
5282
|
+
this.schedule();
|
|
5283
|
+
}
|
|
5284
|
+
timeoutId = null;
|
|
5285
|
+
remainingMs;
|
|
5286
|
+
resumedAt;
|
|
5287
|
+
schedule() {
|
|
5288
|
+
this.timeoutId = setTimeout(() => {
|
|
5289
|
+
this.abortController.abort();
|
|
5290
|
+
}, this.remainingMs);
|
|
5291
|
+
}
|
|
5292
|
+
/** Pause the timeout (e.g. while waiting for user). */
|
|
5293
|
+
pause() {
|
|
5294
|
+
if (this.timeoutId) {
|
|
5295
|
+
clearTimeout(this.timeoutId);
|
|
5296
|
+
this.timeoutId = null;
|
|
5297
|
+
const elapsed = Date.now() - this.resumedAt;
|
|
5298
|
+
this.remainingMs = Math.max(0, this.remainingMs - elapsed);
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
/** Resume the timeout after user interaction completes. */
|
|
5302
|
+
resume() {
|
|
5303
|
+
this.resumedAt = Date.now();
|
|
5304
|
+
this.schedule();
|
|
5305
|
+
}
|
|
5306
|
+
clear() {
|
|
5307
|
+
if (this.timeoutId) {
|
|
5308
|
+
clearTimeout(this.timeoutId);
|
|
5309
|
+
this.timeoutId = null;
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
};
|
|
5255
5313
|
var MAX_HISTORY_ENTRIES = 10;
|
|
5256
5314
|
var MAX_RESPONSE_LENGTH = 1500;
|
|
5257
5315
|
var TaskProcessor = class {
|
|
@@ -5334,12 +5392,16 @@ var TaskProcessor = class {
|
|
|
5334
5392
|
}
|
|
5335
5393
|
systemPrompt += historyPrompt;
|
|
5336
5394
|
}
|
|
5395
|
+
const abortController = new AbortController();
|
|
5396
|
+
const taskTimeout = new TaskTimeout(abortController, taskTimeoutMs);
|
|
5337
5397
|
const browserServer = createBrowserMcpServer();
|
|
5338
5398
|
const agentToolsServer = createAgentToolsServer({
|
|
5339
5399
|
memoryManager: this.memoryManager,
|
|
5340
5400
|
skillManager: this.skillManager,
|
|
5341
5401
|
taskId: task.id,
|
|
5342
|
-
sessionId: this.sessionId || void 0
|
|
5402
|
+
sessionId: this.sessionId || void 0,
|
|
5403
|
+
onUserWaitStart: () => taskTimeout.pause(),
|
|
5404
|
+
onUserWaitEnd: () => taskTimeout.resume()
|
|
5343
5405
|
});
|
|
5344
5406
|
const eventHooks = createEventHooks(task.id, toolCallRecords);
|
|
5345
5407
|
const allowedTools = [
|
|
@@ -5386,7 +5448,6 @@ var TaskProcessor = class {
|
|
|
5386
5448
|
session_id: ""
|
|
5387
5449
|
};
|
|
5388
5450
|
}
|
|
5389
|
-
const abortController = new AbortController();
|
|
5390
5451
|
const options = {
|
|
5391
5452
|
model: config.model,
|
|
5392
5453
|
systemPrompt,
|
|
@@ -5404,9 +5465,6 @@ var TaskProcessor = class {
|
|
|
5404
5465
|
abortController
|
|
5405
5466
|
};
|
|
5406
5467
|
const taskStartTime = Date.now();
|
|
5407
|
-
const timeoutId = setTimeout(() => {
|
|
5408
|
-
abortController.abort();
|
|
5409
|
-
}, taskTimeoutMs);
|
|
5410
5468
|
try {
|
|
5411
5469
|
for await (const message of query2({
|
|
5412
5470
|
prompt: promptMessages(),
|
|
@@ -5468,7 +5526,7 @@ var TaskProcessor = class {
|
|
|
5468
5526
|
}
|
|
5469
5527
|
}
|
|
5470
5528
|
} finally {
|
|
5471
|
-
|
|
5529
|
+
taskTimeout.clear();
|
|
5472
5530
|
}
|
|
5473
5531
|
const MAX_CONTENT_LENGTH = 5e4;
|
|
5474
5532
|
const truncatedResponse = finalResponse.length > MAX_CONTENT_LENGTH ? finalResponse.slice(0, MAX_CONTENT_LENGTH) + "\n\n[Response truncated]" : finalResponse;
|
package/package.json
CHANGED
package/src/agent/processor.ts
CHANGED
|
@@ -31,6 +31,55 @@ import {
|
|
|
31
31
|
import { createEventHooks } from "./event-hooks.js";
|
|
32
32
|
import { BASE_SYSTEM_PROMPT } from "./system-prompt.js";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Manages the task wall-clock timeout.
|
|
36
|
+
* Supports pausing while the agent is waiting for user input (ask_user)
|
|
37
|
+
* so that idle wait time doesn't count toward the timeout.
|
|
38
|
+
*/
|
|
39
|
+
class TaskTimeout {
|
|
40
|
+
private timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
41
|
+
private remainingMs: number;
|
|
42
|
+
private resumedAt: number;
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
private abortController: AbortController,
|
|
46
|
+
timeoutMs: number
|
|
47
|
+
) {
|
|
48
|
+
this.remainingMs = timeoutMs;
|
|
49
|
+
this.resumedAt = Date.now();
|
|
50
|
+
this.schedule();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private schedule(): void {
|
|
54
|
+
this.timeoutId = setTimeout(() => {
|
|
55
|
+
this.abortController.abort();
|
|
56
|
+
}, this.remainingMs);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Pause the timeout (e.g. while waiting for user). */
|
|
60
|
+
pause(): void {
|
|
61
|
+
if (this.timeoutId) {
|
|
62
|
+
clearTimeout(this.timeoutId);
|
|
63
|
+
this.timeoutId = null;
|
|
64
|
+
const elapsed = Date.now() - this.resumedAt;
|
|
65
|
+
this.remainingMs = Math.max(0, this.remainingMs - elapsed);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Resume the timeout after user interaction completes. */
|
|
70
|
+
resume(): void {
|
|
71
|
+
this.resumedAt = Date.now();
|
|
72
|
+
this.schedule();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
clear(): void {
|
|
76
|
+
if (this.timeoutId) {
|
|
77
|
+
clearTimeout(this.timeoutId);
|
|
78
|
+
this.timeoutId = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
34
83
|
const MAX_HISTORY_ENTRIES = 10;
|
|
35
84
|
const MAX_RESPONSE_LENGTH = 1500;
|
|
36
85
|
|
|
@@ -143,6 +192,9 @@ export class TaskProcessor {
|
|
|
143
192
|
systemPrompt += historyPrompt;
|
|
144
193
|
}
|
|
145
194
|
|
|
195
|
+
const abortController = new AbortController();
|
|
196
|
+
const taskTimeout = new TaskTimeout(abortController, taskTimeoutMs);
|
|
197
|
+
|
|
146
198
|
// Create MCP servers for custom tools
|
|
147
199
|
const browserServer = createBrowserMcpServer();
|
|
148
200
|
const agentToolsServer = createAgentToolsServer({
|
|
@@ -150,6 +202,8 @@ export class TaskProcessor {
|
|
|
150
202
|
skillManager: this.skillManager,
|
|
151
203
|
taskId: task.id,
|
|
152
204
|
sessionId: this.sessionId || undefined,
|
|
205
|
+
onUserWaitStart: () => taskTimeout.pause(),
|
|
206
|
+
onUserWaitEnd: () => taskTimeout.resume(),
|
|
153
207
|
});
|
|
154
208
|
|
|
155
209
|
// Create event hooks for Supabase event emission
|
|
@@ -203,7 +257,6 @@ export class TaskProcessor {
|
|
|
203
257
|
};
|
|
204
258
|
}
|
|
205
259
|
|
|
206
|
-
const abortController = new AbortController();
|
|
207
260
|
const options: Options = {
|
|
208
261
|
model: config.model,
|
|
209
262
|
systemPrompt,
|
|
@@ -221,11 +274,7 @@ export class TaskProcessor {
|
|
|
221
274
|
abortController,
|
|
222
275
|
};
|
|
223
276
|
|
|
224
|
-
// Wall-clock timeout via abort
|
|
225
277
|
const taskStartTime = Date.now();
|
|
226
|
-
const timeoutId = setTimeout(() => {
|
|
227
|
-
abortController.abort();
|
|
228
|
-
}, taskTimeoutMs);
|
|
229
278
|
|
|
230
279
|
try {
|
|
231
280
|
for await (const message of query({
|
|
@@ -302,7 +351,7 @@ export class TaskProcessor {
|
|
|
302
351
|
}
|
|
303
352
|
}
|
|
304
353
|
} finally {
|
|
305
|
-
|
|
354
|
+
taskTimeout.clear();
|
|
306
355
|
}
|
|
307
356
|
|
|
308
357
|
// Truncate finalResponse to avoid edge function payload limits
|
|
@@ -41,12 +41,16 @@ Available capabilities:
|
|
|
41
41
|
- Bash tool for shell commands
|
|
42
42
|
- Glob and Grep for file search
|
|
43
43
|
|
|
44
|
-
3. MEMORY:
|
|
44
|
+
3. MEMORY & CREDENTIALS:
|
|
45
45
|
- You can remember things about the user using memory_store
|
|
46
46
|
- Use this when you learn preferences, important facts, or standing instructions
|
|
47
47
|
- Your stored memories persist across conversations
|
|
48
48
|
- PROACTIVELY use memory_store during tasks when you discover user preferences, habits, or important context
|
|
49
49
|
- Before completing a task, consider if anything learned should be remembered for future conversations
|
|
50
|
+
- CRITICAL — Credential Storage: When you create, register, or receive any account credentials (username, password, API keys, tokens), you MUST use credential_set to save them locally. NEVER use memory_store for credentials — memory_store is for preferences and facts, credential_set is for secrets. Examples:
|
|
51
|
+
* After registering a new email/account → credential_set with type "login" and data { "username": "...", "password": "...", "email": "..." }
|
|
52
|
+
* After generating an API key → credential_set with type "api_key" and data { "api_key": "..." }
|
|
53
|
+
* Credentials saved via credential_set are encrypted on disk and viewable in the desktop app's Credentials panel
|
|
50
54
|
|
|
51
55
|
4. SKILL-AWARE EXECUTION (CRITICAL — follow this for EVERY task):
|
|
52
56
|
Step A — Search: Before executing ANY task, check if an existing skill matches (use skill_invoke or skill_search).
|
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
|
{
|