agentmesh-ai 0.2.1 → 0.3.0
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/README.md +127 -144
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +41 -22
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/join.d.ts +1 -5
- package/dist/cli/commands/join.d.ts.map +1 -1
- package/dist/cli/commands/join.js +99 -76
- package/dist/cli/commands/join.js.map +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +8 -1
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +31 -10
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +15 -0
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/index.js +3 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cloud/cloud-conversation.d.ts +83 -0
- package/dist/cloud/cloud-conversation.d.ts.map +1 -0
- package/dist/cloud/cloud-conversation.js +357 -0
- package/dist/cloud/cloud-conversation.js.map +1 -0
- package/dist/cloud/index.d.ts +4 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +3 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/cloud/supabase-client.d.ts +29 -0
- package/dist/cloud/supabase-client.d.ts.map +1 -0
- package/dist/cloud/supabase-client.js +135 -0
- package/dist/cloud/supabase-client.js.map +1 -0
- package/dist/conversation/server.d.ts.map +1 -1
- package/dist/conversation/server.js +6 -0
- package/dist/conversation/server.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/conversation-tools.d.ts +5 -1
- package/dist/mcp/conversation-tools.d.ts.map +1 -1
- package/dist/mcp/conversation-tools.js +202 -149
- package/dist/mcp/conversation-tools.js.map +1 -1
- package/dist/mcp/index.js +94 -36
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/memory-tools.d.ts +20 -2
- package/dist/mcp/memory-tools.d.ts.map +1 -1
- package/dist/mcp/memory-tools.js +88 -44
- package/dist/mcp/memory-tools.js.map +1 -1
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +1 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/merge-view.d.ts.map +1 -1
- package/dist/memory/merge-view.js +5 -4
- package/dist/memory/merge-view.js.map +1 -1
- package/dist/memory/reader.js +2 -2
- package/dist/memory/reader.js.map +1 -1
- package/dist/memory/schema.d.ts +3 -0
- package/dist/memory/schema.d.ts.map +1 -1
- package/dist/memory/schema.js +1 -0
- package/dist/memory/schema.js.map +1 -1
- package/dist/memory/types.d.ts +1 -0
- package/dist/memory/types.d.ts.map +1 -1
- package/dist/memory/write-utils.d.ts +30 -0
- package/dist/memory/write-utils.d.ts.map +1 -0
- package/dist/memory/write-utils.js +98 -0
- package/dist/memory/write-utils.js.map +1 -0
- package/dist/memory/writer.d.ts.map +1 -1
- package/dist/memory/writer.js +21 -84
- package/dist/memory/writer.js.map +1 -1
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +4 -0
- package/dist/utils/id.js.map +1 -1
- package/dist/utils/notify.d.ts +2 -0
- package/dist/utils/notify.d.ts.map +1 -0
- package/dist/utils/notify.js +50 -0
- package/dist/utils/notify.js.map +1 -0
- package/package.json +5 -4
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP tool definitions for conversation space operations.
|
|
3
|
+
*
|
|
4
|
+
* Supports both legacy WebSocket client and cloud Supabase client.
|
|
5
|
+
* The cloud client uses push events (waitForMessage) instead of polling.
|
|
3
6
|
*/
|
|
4
7
|
import { z } from 'zod';
|
|
5
8
|
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { CloudConversationClient } from '../cloud/cloud-conversation.js';
|
|
6
10
|
import { writeMemoryEntry } from '../memory/writer.js';
|
|
7
11
|
import { readHubConfig } from '../memory/reader.js';
|
|
8
|
-
|
|
12
|
+
import { cloudWriteMemory } from '../cloud/supabase-client.js';
|
|
13
|
+
/** Format ISO timestamp as local time + relative ("14:23:05 (3m ago)"). */
|
|
14
|
+
function formatTime(isoTimestamp) {
|
|
15
|
+
const date = new Date(isoTimestamp);
|
|
16
|
+
const diffMin = Math.floor((Date.now() - date.getTime()) / 60000);
|
|
17
|
+
const local = date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
18
|
+
if (diffMin < 1)
|
|
19
|
+
return `${local} (just now)`;
|
|
20
|
+
if (diffMin < 60)
|
|
21
|
+
return `${local} (${diffMin}m ago)`;
|
|
22
|
+
if (diffMin < 1440)
|
|
23
|
+
return `${local} (${Math.floor(diffMin / 60)}h ago)`;
|
|
24
|
+
return `${local} (${Math.floor(diffMin / 1440)}d ago)`;
|
|
25
|
+
}
|
|
26
|
+
export function registerConversationTools(server, getAgentHubDir, getAgentId, getOrConnectClient, getTeamId) {
|
|
27
|
+
const noConnectionMsg = (teamId) => teamId
|
|
28
|
+
? 'Cannot connect to cloud conversation. Check your network connection.'
|
|
29
|
+
: 'No conversation space is running. A human can start one with: npx agentmesh-ai serve';
|
|
9
30
|
server.tool('start_meeting', `Start a meeting — check that all required team members are online before discussing.
|
|
10
31
|
|
|
11
32
|
Use this BEFORE using discuss when you need a team decision. It ensures everyone
|
|
@@ -17,15 +38,10 @@ to come online. You can call start_meeting again to check if they've joined.`, {
|
|
|
17
38
|
requiredAgents: z.array(z.string()).optional().describe('Agent IDs that must attend. If omitted, all agents from hub.yaml are required.'),
|
|
18
39
|
}, async ({ topic, requiredAgents }) => {
|
|
19
40
|
const client = await getOrConnectClient();
|
|
41
|
+
const teamId = getTeamId?.();
|
|
20
42
|
if (!client?.isConnected) {
|
|
21
|
-
return {
|
|
22
|
-
content: [{
|
|
23
|
-
type: 'text',
|
|
24
|
-
text: 'Cannot start meeting: no conversation space is running. A human can start one with: npx agentmesh-ai serve'
|
|
25
|
-
}]
|
|
26
|
-
};
|
|
43
|
+
return { content: [{ type: 'text', text: noConnectionMsg(teamId) }] };
|
|
27
44
|
}
|
|
28
|
-
// If no specific agents given, require all from hub.yaml
|
|
29
45
|
let required = requiredAgents;
|
|
30
46
|
if (!required || required.length === 0) {
|
|
31
47
|
const dir = getAgentHubDir();
|
|
@@ -34,8 +50,10 @@ to come online. You can call start_meeting again to check if they've joined.`, {
|
|
|
34
50
|
}
|
|
35
51
|
const meetingId = `meeting-${randomUUID()}`;
|
|
36
52
|
client.startMeeting(meetingId, topic, required);
|
|
37
|
-
//
|
|
38
|
-
|
|
53
|
+
// Cloud mode: meeting status is computed locally; legacy: wait for server broadcast
|
|
54
|
+
if (!(client instanceof CloudConversationClient)) {
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
56
|
+
}
|
|
39
57
|
const status = client.meetings.find(m => m.meetingId === meetingId);
|
|
40
58
|
if (!status) {
|
|
41
59
|
return {
|
|
@@ -67,18 +85,16 @@ to come online. You can call start_meeting again to check if they've joined.`, {
|
|
|
67
85
|
Returns the list of online agents and what topics are being discussed.
|
|
68
86
|
If no conversation space is running, tells you so.`, {}, async () => {
|
|
69
87
|
const client = await getOrConnectClient();
|
|
88
|
+
const teamId = getTeamId?.();
|
|
70
89
|
if (!client?.isConnected) {
|
|
71
|
-
return {
|
|
72
|
-
content: [{
|
|
73
|
-
type: 'text',
|
|
74
|
-
text: 'No conversation space is running. A human can start one with: npx agentmesh-ai serve'
|
|
75
|
-
}]
|
|
76
|
-
};
|
|
90
|
+
return { content: [{ type: 'text', text: noConnectionMsg(teamId) }] };
|
|
77
91
|
}
|
|
78
92
|
const agents = client.agents;
|
|
79
93
|
const topics = client.topics;
|
|
80
94
|
const myId = getAgentId();
|
|
81
95
|
const lines = [];
|
|
96
|
+
const mode = teamId ? '☁️ Cloud' : '🔌 Local';
|
|
97
|
+
lines.push(`${mode} conversation space\n`);
|
|
82
98
|
lines.push(`Connected agents (${agents.length}):`);
|
|
83
99
|
for (const a of agents) {
|
|
84
100
|
lines.push(` - ${a.displayName} (${a.role}) using ${a.tool}`);
|
|
@@ -90,7 +106,7 @@ If no conversation space is running, tells you so.`, {}, async () => {
|
|
|
90
106
|
const msgs = client.getTopicMessages(t);
|
|
91
107
|
const participants = [...new Set(msgs.map(m => m.from.displayName))];
|
|
92
108
|
const lastMsg = msgs[msgs.length - 1];
|
|
93
|
-
const lastTime = lastMsg ? lastMsg.timestamp
|
|
109
|
+
const lastTime = lastMsg ? formatTime(lastMsg.timestamp) : '?';
|
|
94
110
|
const myParticipation = msgs.some(m => m.from.id === myId) ? '✓ you spoke' : '⏳ waiting for you';
|
|
95
111
|
lines.push(` - "${t}" — ${msgs.length} msgs, participants: ${participants.join(', ')}, last: ${lastTime} (${myParticipation})`);
|
|
96
112
|
}
|
|
@@ -109,25 +125,19 @@ Your message will be seen by all connected agents.`, {
|
|
|
109
125
|
related_to: z.string().optional().describe('Memory topic or file path this relates to'),
|
|
110
126
|
}, async ({ topic, message, related_to }) => {
|
|
111
127
|
const client = await getOrConnectClient();
|
|
128
|
+
const teamId = getTeamId?.();
|
|
112
129
|
if (!client?.isConnected) {
|
|
113
|
-
return {
|
|
114
|
-
content: [{
|
|
115
|
-
type: 'text',
|
|
116
|
-
text: 'Cannot send message: no conversation space is running. A human can start one with: npx agentmesh-ai serve'
|
|
117
|
-
}]
|
|
118
|
-
};
|
|
130
|
+
return { content: [{ type: 'text', text: noConnectionMsg(teamId) }] };
|
|
119
131
|
}
|
|
120
132
|
client.sendMessage(topic, message, related_to);
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
// Legacy mode: wait for server broadcast; cloud mode: skip
|
|
134
|
+
if (!(client instanceof CloudConversationClient)) {
|
|
135
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
136
|
+
}
|
|
124
137
|
const recent = client.getTopicMessages(topic).slice(-10);
|
|
125
138
|
if (recent.length === 0) {
|
|
126
139
|
return {
|
|
127
|
-
content: [{
|
|
128
|
-
type: 'text',
|
|
129
|
-
text: `Message sent to topic "${topic}". No replies yet.`
|
|
130
|
-
}]
|
|
140
|
+
content: [{ type: 'text', text: `Message sent to topic "${topic}". No replies yet.` }]
|
|
131
141
|
};
|
|
132
142
|
}
|
|
133
143
|
const lines = [`Messages on "${topic}":\n`];
|
|
@@ -142,13 +152,9 @@ Use this to check if other agents have replied to your earlier message.`, {
|
|
|
142
152
|
since: z.string().optional().describe('Only get messages after this ISO timestamp'),
|
|
143
153
|
}, async ({ topic, since }) => {
|
|
144
154
|
const client = await getOrConnectClient();
|
|
155
|
+
const teamId = getTeamId?.();
|
|
145
156
|
if (!client?.isConnected) {
|
|
146
|
-
return {
|
|
147
|
-
content: [{
|
|
148
|
-
type: 'text',
|
|
149
|
-
text: 'No conversation space is running.'
|
|
150
|
-
}]
|
|
151
|
-
};
|
|
157
|
+
return { content: [{ type: 'text', text: noConnectionMsg(teamId) }] };
|
|
152
158
|
}
|
|
153
159
|
let messages = client.getTopicMessages(topic);
|
|
154
160
|
if (since) {
|
|
@@ -157,15 +163,12 @@ Use this to check if other agents have replied to your earlier message.`, {
|
|
|
157
163
|
}
|
|
158
164
|
if (messages.length === 0) {
|
|
159
165
|
return {
|
|
160
|
-
content: [{
|
|
161
|
-
type: 'text',
|
|
162
|
-
text: `No messages on "${topic}"${since ? ' since ' + since : ''}.`
|
|
163
|
-
}]
|
|
166
|
+
content: [{ type: 'text', text: `No messages on "${topic}"${since ? ' since ' + since : ''}.` }]
|
|
164
167
|
};
|
|
165
168
|
}
|
|
166
169
|
const lines = [`Messages on "${topic}" (${messages.length}):\n`];
|
|
167
170
|
for (const m of messages) {
|
|
168
|
-
const time = m.timestamp
|
|
171
|
+
const time = formatTime(m.timestamp);
|
|
169
172
|
lines.push(`[${time}] ${m.from.displayName}: ${m.content}`);
|
|
170
173
|
}
|
|
171
174
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
@@ -177,9 +180,15 @@ when you want a real back-and-forth conversation.
|
|
|
177
180
|
|
|
178
181
|
HOW IT WORKS:
|
|
179
182
|
1. You send your message to the topic
|
|
180
|
-
2. The tool waits for other agents to reply
|
|
181
|
-
3.
|
|
182
|
-
4.
|
|
183
|
+
2. The tool waits for other agents to reply
|
|
184
|
+
3. If someone replies "hold on" / "wait" / "等一下", it automatically extends the wait
|
|
185
|
+
4. Returns ALL replies so you can see the full discussion
|
|
186
|
+
5. You then decide: call discuss again to continue, or save_to_memory to conclude
|
|
187
|
+
|
|
188
|
+
WAIT TIME:
|
|
189
|
+
- Default: 60 seconds. Max: 1800 seconds (30 minutes).
|
|
190
|
+
- If the human says something like "wait for 10 minutes", set wait_seconds to 600.
|
|
191
|
+
- If someone sends a "hold on" message, the wait is automatically extended by 5 minutes.
|
|
183
192
|
|
|
184
193
|
WHEN TO USE:
|
|
185
194
|
- You need team consensus on a decision (tech stack, architecture, conventions)
|
|
@@ -191,50 +200,32 @@ You drive the discussion loop yourself — keep calling discuss until consensus
|
|
|
191
200
|
then call save_to_memory to record the conclusion.
|
|
192
201
|
|
|
193
202
|
ASYNC FALLBACK:
|
|
194
|
-
If
|
|
195
|
-
you can discuss asynchronously through shared memory:
|
|
203
|
+
If no other agents are online, you can discuss asynchronously through shared memory:
|
|
196
204
|
1. Write your proposal with write_memory, topic prefix "Discussion: ", status "proposal"
|
|
197
205
|
2. Other agents will see it when they read_memory (marked 📋)
|
|
198
206
|
3. They can respond by writing their own entries on the same topic
|
|
199
207
|
4. Once consensus forms in memory, upgrade the conclusion to status "protected"`, {
|
|
200
208
|
topic: z.string().describe('Discussion topic'),
|
|
201
209
|
message: z.string().describe('Your message — proposal, question, or response to others'),
|
|
202
|
-
wait_seconds: z.number().optional().describe('How long to wait for replies (default:
|
|
210
|
+
wait_seconds: z.number().optional().describe('How long to wait for replies (default: 60, max: 1800 = 30 min). Set higher if the human asks to wait longer.'),
|
|
203
211
|
related_to: z.string().optional().describe('Memory topic or file path this discussion relates to'),
|
|
204
212
|
}, async ({ topic, message, wait_seconds, related_to }) => {
|
|
205
213
|
const client = await getOrConnectClient();
|
|
214
|
+
const teamId = getTeamId?.();
|
|
206
215
|
if (!client?.isConnected) {
|
|
207
|
-
return {
|
|
208
|
-
content: [{
|
|
209
|
-
type: 'text',
|
|
210
|
-
text: 'Cannot discuss: no conversation space is running. A human can start one with: npx agentmesh-ai serve'
|
|
211
|
-
}]
|
|
212
|
-
};
|
|
216
|
+
return { content: [{ type: 'text', text: noConnectionMsg(teamId) }] };
|
|
213
217
|
}
|
|
214
|
-
// Record the timestamp before sending so we can filter for new replies
|
|
215
218
|
const beforeSend = new Date().toISOString();
|
|
216
|
-
// Send our message
|
|
217
219
|
client.sendMessage(topic, message, related_to);
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
await
|
|
227
|
-
const newMessages = client.getTopicMessages(topic).filter(m => new Date(m.timestamp).getTime() > new Date(beforeSend).getTime()
|
|
228
|
-
&& m.from.id !== getAgentId());
|
|
229
|
-
if (newMessages.length > lastCount) {
|
|
230
|
-
// New replies came in — reset silence timer
|
|
231
|
-
lastCount = newMessages.length;
|
|
232
|
-
silentSince = Date.now();
|
|
233
|
-
}
|
|
234
|
-
else if (Date.now() - silentSince > 15000 && lastCount > 0) {
|
|
235
|
-
// 15 seconds of silence after getting at least one reply — discussion round is done
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
220
|
+
const waitMs = Math.min((wait_seconds ?? 60), 1800) * 1000;
|
|
221
|
+
const myId = getAgentId();
|
|
222
|
+
// Cloud mode: use push events (waitForMessage)
|
|
223
|
+
// Legacy mode: poll every 3 seconds
|
|
224
|
+
if (client instanceof CloudConversationClient) {
|
|
225
|
+
await waitForRepliesCloud(client, topic, myId, waitMs);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
await waitForRepliesPolling(client, topic, myId, beforeSend, waitMs);
|
|
238
229
|
}
|
|
239
230
|
// Collect all messages on this topic
|
|
240
231
|
const allMessages = client.getTopicMessages(topic);
|
|
@@ -242,7 +233,7 @@ you can discuss asynchronously through shared memory:
|
|
|
242
233
|
return {
|
|
243
234
|
content: [{
|
|
244
235
|
type: 'text',
|
|
245
|
-
text: `Your message was sent to "${topic}". No other agents replied within ${wait_seconds ??
|
|
236
|
+
text: `Your message was sent to "${topic}". No other agents replied within ${wait_seconds ?? 60}s. They may be offline — the message will be there when they check.`
|
|
246
237
|
}]
|
|
247
238
|
};
|
|
248
239
|
}
|
|
@@ -250,13 +241,12 @@ you can discuss asynchronously through shared memory:
|
|
|
250
241
|
const lines = [];
|
|
251
242
|
lines.push(`Discussion on "${topic}" (${allMessages.length} messages):\n`);
|
|
252
243
|
for (const m of allMessages) {
|
|
253
|
-
const time = m.timestamp
|
|
254
|
-
const isMe = m.from.id ===
|
|
244
|
+
const time = formatTime(m.timestamp);
|
|
245
|
+
const isMe = m.from.id === myId;
|
|
255
246
|
const prefix = isMe ? '(you)' : `${m.from.displayName} [${m.from.role}]`;
|
|
256
247
|
lines.push(`[${time}] ${prefix}: ${m.content}`);
|
|
257
248
|
}
|
|
258
|
-
|
|
259
|
-
const others = new Set(allMessages.filter(m => m.from.id !== getAgentId()).map(m => m.from.id));
|
|
249
|
+
const others = new Set(allMessages.filter(m => m.from.id !== myId).map(m => m.from.id));
|
|
260
250
|
lines.push('');
|
|
261
251
|
lines.push(`--- ${others.size} other agent(s) participated ---`);
|
|
262
252
|
lines.push('');
|
|
@@ -278,13 +268,17 @@ This records the conclusion permanently so all agents (including offline ones) w
|
|
|
278
268
|
const agentId = getAgentId();
|
|
279
269
|
const hub = await readHubConfig(dir);
|
|
280
270
|
const agentInfo = hub?.agents.find(a => a.id === agentId);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
271
|
+
const role = agentInfo?.role ?? 'developer';
|
|
272
|
+
const tool = agentInfo?.tool ?? 'unknown';
|
|
273
|
+
// Write to local + cloud in parallel, and send conclusion
|
|
274
|
+
const teamId = getTeamId?.() ?? hub?.team_id;
|
|
275
|
+
const writeOpts = { topic, content, tags, decided_with };
|
|
276
|
+
await Promise.all([
|
|
277
|
+
writeMemoryEntry(dir, agentId, role, tool, writeOpts),
|
|
278
|
+
teamId ? cloudWriteMemory(teamId, agentId, role, tool, writeOpts).catch(() => { }) : Promise.resolve(),
|
|
279
|
+
getOrConnectClient().then(client => { if (client?.isConnected)
|
|
280
|
+
client.conclude(topic, content, tags); }),
|
|
281
|
+
]);
|
|
288
282
|
return {
|
|
289
283
|
content: [{
|
|
290
284
|
type: 'text',
|
|
@@ -326,13 +320,10 @@ Flow:
|
|
|
326
320
|
'',
|
|
327
321
|
recommendation ? `💡 Recommendation: ${recommendation}` : '',
|
|
328
322
|
].filter(Boolean).join('\n');
|
|
329
|
-
// Generate a unique decision ID
|
|
330
323
|
const decisionId = `decision-${randomUUID()}`;
|
|
331
|
-
// Broadcast proposal to conversation space (so other agents see it)
|
|
332
324
|
if (client?.isConnected) {
|
|
333
325
|
client.proposeDecision(decisionId, title, options, recommendation);
|
|
334
326
|
}
|
|
335
|
-
// Ask THIS human via elicitation
|
|
336
327
|
try {
|
|
337
328
|
const result = await server.server.elicitInput({
|
|
338
329
|
mode: 'form',
|
|
@@ -369,7 +360,6 @@ Flow:
|
|
|
369
360
|
const reason = result.content.reason;
|
|
370
361
|
if (choice === '__none_of_the_above__') {
|
|
371
362
|
const userDirection = reason || 'No specific direction given';
|
|
372
|
-
// Vote reject and broadcast the reason to the conversation space
|
|
373
363
|
if (client?.isConnected) {
|
|
374
364
|
client.vote(decisionId, 'reject', undefined, userDirection);
|
|
375
365
|
client.sendMessage(title, `❌ Human rejected all proposed options. Feedback: "${userDirection}". Discussion should restart with this new direction.`);
|
|
@@ -381,13 +371,11 @@ Flow:
|
|
|
381
371
|
}]
|
|
382
372
|
};
|
|
383
373
|
}
|
|
384
|
-
// Vote approve in the conversation space
|
|
385
374
|
if (client?.isConnected) {
|
|
386
375
|
client.vote(decisionId, 'approve', choice, reason);
|
|
387
376
|
// Wait for other votes
|
|
388
|
-
const
|
|
377
|
+
const deadline = Date.now() + 60000;
|
|
389
378
|
const pollInterval = 3000;
|
|
390
|
-
const deadline = Date.now() + waitMs;
|
|
391
379
|
while (Date.now() < deadline) {
|
|
392
380
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
393
381
|
const resolved = client.resolvedDecisions.find(d => d.decisionId === decisionId);
|
|
@@ -411,7 +399,6 @@ Flow:
|
|
|
411
399
|
}
|
|
412
400
|
}
|
|
413
401
|
}
|
|
414
|
-
// Timeout — not all voted yet
|
|
415
402
|
return {
|
|
416
403
|
content: [{
|
|
417
404
|
type: 'text',
|
|
@@ -419,7 +406,6 @@ Flow:
|
|
|
419
406
|
}]
|
|
420
407
|
};
|
|
421
408
|
}
|
|
422
|
-
// No conversation space — single user decision
|
|
423
409
|
return {
|
|
424
410
|
content: [{
|
|
425
411
|
type: 'text',
|
|
@@ -429,20 +415,13 @@ Flow:
|
|
|
429
415
|
}
|
|
430
416
|
else {
|
|
431
417
|
return {
|
|
432
|
-
content: [{
|
|
433
|
-
type: 'text',
|
|
434
|
-
text: '⏸️ User declined to vote right now. Continue with other work and come back to this later.',
|
|
435
|
-
}]
|
|
418
|
+
content: [{ type: 'text', text: '⏸️ User declined to vote right now. Continue with other work and come back to this later.' }]
|
|
436
419
|
};
|
|
437
420
|
}
|
|
438
421
|
}
|
|
439
422
|
catch {
|
|
440
|
-
// Elicitation not supported — fall back to text
|
|
441
423
|
return {
|
|
442
|
-
content: [{
|
|
443
|
-
type: 'text',
|
|
444
|
-
text: `❓ Decision needed: ${title}\n\n${message}\n\nPlease tell me which option you prefer.`,
|
|
445
|
-
}]
|
|
424
|
+
content: [{ type: 'text', text: `❓ Decision needed: ${title}\n\n${message}\n\nPlease tell me which option you prefer.` }]
|
|
446
425
|
};
|
|
447
426
|
}
|
|
448
427
|
});
|
|
@@ -468,73 +447,147 @@ Flow:
|
|
|
468
447
|
});
|
|
469
448
|
server.tool('vote_on_decision', `Vote on a pending team decision that was proposed by another agent.
|
|
470
449
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
450
|
+
You can vote in two ways:
|
|
451
|
+
1. If the user already told you their choice, pass it directly via the "choice" parameter
|
|
452
|
+
2. If you need to ask the user, leave "choice" empty and this tool will try to show a form
|
|
453
|
+
|
|
454
|
+
Always ask the user which option they prefer BEFORE calling this tool, then pass their answer.`, {
|
|
474
455
|
decisionId: z.string().describe('The decision ID from the proposal'),
|
|
475
|
-
topic: z.string().describe('The decision topic
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
recommendation: z.string().optional().describe('The proposer\'s recommendation'),
|
|
481
|
-
}, async ({ decisionId, topic, options, recommendation }) => {
|
|
456
|
+
topic: z.string().describe('The decision topic'),
|
|
457
|
+
choice: z.string().optional().describe('The chosen option (e.g. "PostgreSQL"). If provided, votes directly without showing a form.'),
|
|
458
|
+
vote: z.enum(['approve', 'reject']).optional().describe('approve or reject. Default: approve if choice is given.'),
|
|
459
|
+
reason: z.string().optional().describe('Reason for the vote'),
|
|
460
|
+
}, async ({ decisionId, topic, choice, vote, reason }) => {
|
|
482
461
|
const client = await getOrConnectClient();
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
''
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
462
|
+
if (choice) {
|
|
463
|
+
const voteType = (choice === '__none_of_the_above__' || vote === 'reject') ? 'reject' : 'approve';
|
|
464
|
+
client?.vote(decisionId, voteType, voteType === 'approve' ? choice : undefined, reason);
|
|
465
|
+
if (voteType === 'reject') {
|
|
466
|
+
return { content: [{ type: 'text', text: `❌ Vote: rejected. ${reason ? `Reason: ${reason}` : ''}` }] };
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
content: [{ type: 'text', text: `✅ Vote: approved "${choice}". ${reason ? `Reason: ${reason}` : ''}\nWaiting for other team members...` }]
|
|
470
|
+
};
|
|
471
|
+
}
|
|
492
472
|
try {
|
|
493
473
|
const result = await server.server.elicitInput({
|
|
494
474
|
mode: 'form',
|
|
495
|
-
message
|
|
475
|
+
message: `Team decision: ${topic}`,
|
|
496
476
|
requestedSchema: {
|
|
497
477
|
type: 'object',
|
|
498
478
|
properties: {
|
|
499
|
-
choice: {
|
|
500
|
-
|
|
501
|
-
title: topic,
|
|
502
|
-
oneOf: [
|
|
503
|
-
...optionLabels.map(label => ({ const: label, title: label })),
|
|
504
|
-
{ const: '__none_of_the_above__', title: 'None of the above — reject' },
|
|
505
|
-
],
|
|
506
|
-
},
|
|
507
|
-
reason: {
|
|
508
|
-
type: 'string',
|
|
509
|
-
title: 'Reason (optional)',
|
|
510
|
-
},
|
|
479
|
+
choice: { type: 'string', title: topic },
|
|
480
|
+
reason: { type: 'string', title: 'Reason (optional)' },
|
|
511
481
|
},
|
|
512
482
|
required: ['choice'],
|
|
513
483
|
},
|
|
514
484
|
});
|
|
515
485
|
if (result.action === 'accept' && result.content) {
|
|
516
|
-
const
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
client?.vote(decisionId, 'reject', undefined, reason || 'Rejected all options');
|
|
520
|
-
return {
|
|
521
|
-
content: [{ type: 'text', text: `❌ Vote: rejected. ${reason ? `Reason: ${reason}` : ''}` }]
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
client?.vote(decisionId, 'approve', choice, reason);
|
|
486
|
+
const userChoice = result.content.choice;
|
|
487
|
+
const userReason = result.content.reason;
|
|
488
|
+
client?.vote(decisionId, 'approve', userChoice, userReason);
|
|
525
489
|
return {
|
|
526
|
-
content: [{ type: 'text', text: `✅ Vote: approved "${
|
|
490
|
+
content: [{ type: 'text', text: `✅ Vote: approved "${userChoice}". Waiting for other team members...` }]
|
|
527
491
|
};
|
|
528
492
|
}
|
|
529
493
|
return {
|
|
530
|
-
content: [{ type: 'text', text: '⏸️ User skipped voting
|
|
494
|
+
content: [{ type: 'text', text: '⏸️ User skipped voting. Ask the user which option they prefer, then call this tool again with their choice.' }]
|
|
531
495
|
};
|
|
532
496
|
}
|
|
533
497
|
catch {
|
|
534
498
|
return {
|
|
535
|
-
content: [{ type: 'text', text: `❓
|
|
499
|
+
content: [{ type: 'text', text: `❓ Could not show vote form. Please ask the user which option they prefer for "${topic}", then call vote_on_decision again with their choice.` }]
|
|
536
500
|
};
|
|
537
501
|
}
|
|
538
502
|
});
|
|
539
503
|
}
|
|
504
|
+
// ── Wait strategies ──────────────────────────────────────────────────────────
|
|
505
|
+
/** How long to extend when someone says "hold on" (5 minutes). */
|
|
506
|
+
const HOLD_ON_EXTENSION_MS = 5 * 60 * 1000;
|
|
507
|
+
/** Patterns that mean "wait for me, I'm coming". */
|
|
508
|
+
const HOLD_ON_PATTERNS = [
|
|
509
|
+
/hold\s*on/i,
|
|
510
|
+
/wait/i,
|
|
511
|
+
/one\s*(moment|minute|sec)/i,
|
|
512
|
+
/coming/i,
|
|
513
|
+
/be\s*right\s*(there|back)/i,
|
|
514
|
+
/brb/i,
|
|
515
|
+
/give\s*me\s*(a\s*)?(moment|minute|sec)/i,
|
|
516
|
+
/等一下/,
|
|
517
|
+
/稍等/,
|
|
518
|
+
/马上/,
|
|
519
|
+
/等等/,
|
|
520
|
+
/来了/,
|
|
521
|
+
/等我/,
|
|
522
|
+
];
|
|
523
|
+
function isHoldOnMessage(content) {
|
|
524
|
+
return HOLD_ON_PATTERNS.some(p => p.test(content));
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Cloud mode: use push events via waitForMessage.
|
|
528
|
+
* If someone says "hold on", automatically extends the deadline.
|
|
529
|
+
*/
|
|
530
|
+
async function waitForRepliesCloud(client, topic, myId, waitMs) {
|
|
531
|
+
let deadline = Date.now() + waitMs;
|
|
532
|
+
let gotRealReply = false;
|
|
533
|
+
while (Date.now() < deadline) {
|
|
534
|
+
const remaining = deadline - Date.now();
|
|
535
|
+
if (remaining <= 0)
|
|
536
|
+
break;
|
|
537
|
+
// After getting a real reply, use shorter silence timeout (15s)
|
|
538
|
+
const timeout = gotRealReply ? Math.min(15000, remaining) : remaining;
|
|
539
|
+
try {
|
|
540
|
+
const msg = await client.waitForMessage(topic, timeout);
|
|
541
|
+
if (isHoldOnMessage(msg.content)) {
|
|
542
|
+
// Extend deadline — someone is coming
|
|
543
|
+
deadline = Math.max(deadline, Date.now() + HOLD_ON_EXTENSION_MS);
|
|
544
|
+
// Don't count "hold on" as a real reply for silence detection
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
gotRealReply = true;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
// Timeout
|
|
552
|
+
if (gotRealReply)
|
|
553
|
+
break;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Legacy mode: poll every 3 seconds (for WebSocket ConversationClient).
|
|
560
|
+
* Also supports "hold on" detection.
|
|
561
|
+
*/
|
|
562
|
+
async function waitForRepliesPolling(client, topic, myId, beforeSend, waitMs) {
|
|
563
|
+
const pollInterval = 3000;
|
|
564
|
+
let deadline = Date.now() + waitMs;
|
|
565
|
+
let lastCount = 0;
|
|
566
|
+
let silentSince = Date.now();
|
|
567
|
+
while (Date.now() < deadline) {
|
|
568
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
569
|
+
const newMessages = client.getTopicMessages(topic).filter(m => new Date(m.timestamp).getTime() > new Date(beforeSend).getTime()
|
|
570
|
+
&& m.from.id !== myId);
|
|
571
|
+
if (newMessages.length > lastCount) {
|
|
572
|
+
const latestNew = newMessages.slice(lastCount);
|
|
573
|
+
lastCount = newMessages.length;
|
|
574
|
+
// Check if any new message is "hold on"
|
|
575
|
+
const hasHoldOn = latestNew.some(m => isHoldOnMessage(m.content));
|
|
576
|
+
if (hasHoldOn) {
|
|
577
|
+
deadline = Math.max(deadline, Date.now() + HOLD_ON_EXTENSION_MS);
|
|
578
|
+
}
|
|
579
|
+
// Check if there's a real (non-hold-on) reply
|
|
580
|
+
const hasRealReply = latestNew.some(m => !isHoldOnMessage(m.content));
|
|
581
|
+
if (hasRealReply) {
|
|
582
|
+
silentSince = Date.now();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else if (Date.now() - silentSince > 15000 && lastCount > 0) {
|
|
586
|
+
// 15 seconds of silence after real replies — round done
|
|
587
|
+
const hasAnyRealReply = newMessages.some(m => !isHoldOnMessage(m.content));
|
|
588
|
+
if (hasAnyRealReply)
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
540
593
|
//# sourceMappingURL=conversation-tools.js.map
|