dsh-a2a-groups 0.0.0-stage → 0.1.2
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-ui.md +350 -0
- package/README.md +160 -2
- package/bin/agent-groups.js +438 -0
- package/client.js +3782 -0
- package/cordis.patch.yml +17 -0
- package/docs/ARCHITECTURE.md +281 -0
- package/docs/BACKLOG.md +201 -0
- package/docs/CONTRACT.md +592 -0
- package/docs/DEPLOY.md +645 -0
- package/docs/DESIGN.md +390 -0
- package/docs/MCP.md +260 -0
- package/docs/PRODUCT.md +186 -0
- package/docs/RUNBOOK.md +406 -0
- package/lib/core/bridge.js +1167 -0
- package/lib/core/card.js +217 -0
- package/lib/core/colleague-cards.js +149 -0
- package/lib/core/config.js +235 -0
- package/lib/core/endpoint.js +69 -0
- package/lib/core/groups.js +3010 -0
- package/lib/core/invite.js +412 -0
- package/lib/core/memberships.js +366 -0
- package/lib/core/probe.js +219 -0
- package/lib/core/registry.js +344 -0
- package/lib/core/server.js +1304 -0
- package/lib/core/skills-group.js +1164 -0
- package/lib/core/skills-session.js +116 -0
- package/lib/core/skills.js +76 -0
- package/lib/core/wire.js +772 -0
- package/lib/dsh/deliver.js +601 -0
- package/lib/dsh/inbox.js +658 -0
- package/lib/dsh/profile.js +195 -0
- package/lib/dsh/prompt.js +55 -0
- package/lib/dsh/sessions.js +278 -0
- package/lib/dsh/toolkit.js +166 -0
- package/lib/dsh/tools.js +101 -0
- package/lib/index.js +648 -0
- package/lib/mcp/mcp-deps.js +145 -0
- package/lib/mcp-server.js +261 -0
- package/lib/shared/tool-runtime.js +1349 -0
- package/lib/shared/tool-specs.js +1123 -0
- package/lib/types/client.d.ts +144 -0
- package/package.json +84 -4
|
@@ -0,0 +1,1123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every tool this package offers, as pure data.
|
|
3
|
+
*
|
|
4
|
+
* A spec is what a host needs in order to OFFER a tool: name, the description a model
|
|
5
|
+
* reads to decide whether to call it, the JSON Schema for its arguments, and the JSON
|
|
6
|
+
* Schema plus renderer for its result.
|
|
7
|
+
*
|
|
8
|
+
* `runtime` names the handler in tool-runtime.js. That indirection is what lets two
|
|
9
|
+
* hosts share one definition:
|
|
10
|
+
*
|
|
11
|
+
* - tools.js adapts specs to `ctx.tools.register` (DSH)
|
|
12
|
+
* - mcp-server.js adapts specs to `tools/list` and `tools/call` (MCP)
|
|
13
|
+
*
|
|
14
|
+
* The description and schemas live here rather than in either adapter, which is what
|
|
15
|
+
* guarantees the two hosts offer the SAME tools with the same contracts: a model that
|
|
16
|
+
* learned `a2a_group_post` inside DSH finds the same shape in an MCP client.
|
|
17
|
+
*
|
|
18
|
+
* `render` stays here too. It is a pure function of (args, value) -> content parts,
|
|
19
|
+
* depends on neither host, and an adapter that reimplemented it would drift.
|
|
20
|
+
*
|
|
21
|
+
* @module dsh-a2a/tool-specs
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Every tool spec, in registration order. */
|
|
25
|
+
export const TOOL_SPECS = Object.freeze([
|
|
26
|
+
{
|
|
27
|
+
|
|
28
|
+
name: 'a2a_agents',
|
|
29
|
+
description:
|
|
30
|
+
'List, add, or remove the remote A2A agents this Session knows about. Each entry names an agent (a colleague\'s DSH instance, a group, or any A2A agent) and how to reach it: through the gateway (`gatewayBase` + `agent`) or directly (`baseUrl`). Run action=list first; add only when the user gave you an address or a link.',
|
|
31
|
+
parameters: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
properties: {
|
|
35
|
+
action: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
enum: ['list', 'add', 'remove'],
|
|
38
|
+
description: 'What to do. "list" shows every registered agent; "add" registers one; "remove" forgets one.',
|
|
39
|
+
},
|
|
40
|
+
name: { type: 'string', description: 'Local name for the agent, used by a2a_send and a2a_discover.' },
|
|
41
|
+
gatewayBase: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'AISIX gateway base including its path, e.g. "http://gw:3000/a2a". "/<agent>" is appended.',
|
|
44
|
+
},
|
|
45
|
+
agent: { type: 'string', description: 'The agent name as registered in the gateway.' },
|
|
46
|
+
baseUrl: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
description: 'Direct A2A endpoint for an agent not behind the gateway, e.g. "http://host:3091/a2a".',
|
|
49
|
+
},
|
|
50
|
+
apiKey: { type: 'string', description: 'Gateway API key for this agent. Omit to use the configured default.' },
|
|
51
|
+
note: { type: 'string', description: 'Optional note about who or what this agent is.' },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
output: {
|
|
55
|
+
schema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
properties: {
|
|
59
|
+
agents: {
|
|
60
|
+
type: 'array',
|
|
61
|
+
items: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
properties: {
|
|
65
|
+
name: { type: 'string' },
|
|
66
|
+
baseUrl: { type: 'string' },
|
|
67
|
+
enabled: { type: 'boolean' },
|
|
68
|
+
note: { type: 'string' },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
changed: { type: 'string' },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
render: (args, value) => [{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: (value.agents ?? []).length === 0
|
|
78
|
+
? 'No A2A agents registered yet. Add one with a2a_agents action=add, or join a group with an invite link.'
|
|
79
|
+
: `Registered A2A agents (${value.agents.length}):\n`
|
|
80
|
+
+ value.agents.map((item) => `- ${item.name} → ${item.baseUrl}${item.enabled === false ? ' (disabled)' : ''}`).join('\n'),
|
|
81
|
+
}],
|
|
82
|
+
},
|
|
83
|
+
runtime: 'a2a_agents',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
|
|
87
|
+
name: 'a2a_discover',
|
|
88
|
+
description:
|
|
89
|
+
'Fetch a remote A2A agent\'s card and show what it can do: its skills, whether it streams, and the exact skill ids a2a_send accepts. Always discover before the first send — a wrong skill id is a rejected call, not a retryable one.',
|
|
90
|
+
parameters: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
properties: {
|
|
94
|
+
agent: { type: 'string', description: 'A registered agent name from a2a_agents, or a raw A2A base URL.' },
|
|
95
|
+
},
|
|
96
|
+
required: ['agent'],
|
|
97
|
+
},
|
|
98
|
+
output: {
|
|
99
|
+
schema: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
additionalProperties: false,
|
|
102
|
+
properties: {
|
|
103
|
+
name: { type: 'string' },
|
|
104
|
+
url: { type: 'string' },
|
|
105
|
+
protocolVersion: { type: 'string' },
|
|
106
|
+
streaming: { type: 'boolean' },
|
|
107
|
+
skills: {
|
|
108
|
+
type: 'array',
|
|
109
|
+
items: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
properties: {
|
|
113
|
+
id: { type: 'string' },
|
|
114
|
+
name: { type: 'string' },
|
|
115
|
+
description: { type: 'string' },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
render: (args, value) => [{
|
|
122
|
+
type: 'text',
|
|
123
|
+
text: `${value.name} (A2A ${value.protocolVersion}, streaming=${value.streaming === true})\n`
|
|
124
|
+
+ `endpoint: ${value.url}\n\nskills:\n`
|
|
125
|
+
+ (value.skills ?? []).map((skill) => `- ${skill.id} — ${skill.description || skill.name}`).join('\n'),
|
|
126
|
+
}],
|
|
127
|
+
},
|
|
128
|
+
runtime: 'a2a_discover',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
|
|
132
|
+
name: 'a2a_send',
|
|
133
|
+
description:
|
|
134
|
+
'Send a task to a remote A2A agent and wait for its answer. Discover the agent first so you name a real skill. The remote agent does not share this conversation, so write a self-contained message. For anything that may take minutes, prefer a2a_stream so progress is visible.',
|
|
135
|
+
parameters: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
additionalProperties: false,
|
|
138
|
+
properties: {
|
|
139
|
+
agent: { type: 'string', description: 'A registered agent name or a raw A2A base URL.' },
|
|
140
|
+
skill: { type: 'string', description: 'Skill id from a2a_discover, e.g. "session/ask".' },
|
|
141
|
+
message: { type: 'string', description: 'The task text. Self-contained: the receiver has no context from this conversation.' },
|
|
142
|
+
data: { type: 'string', description: 'Optional JSON object, as a string, for skills that need structured input.' },
|
|
143
|
+
timeoutMs: { type: 'integer', description: 'How long to wait for an answer, in milliseconds.' },
|
|
144
|
+
},
|
|
145
|
+
required: ['agent', 'message'],
|
|
146
|
+
},
|
|
147
|
+
output: {
|
|
148
|
+
schema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
additionalProperties: false,
|
|
151
|
+
properties: {
|
|
152
|
+
state: { type: 'string' },
|
|
153
|
+
text: { type: 'string' },
|
|
154
|
+
taskId: { type: 'string' },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
render: (args, value) => [{
|
|
158
|
+
type: 'text',
|
|
159
|
+
text: `[${value.state}] ${value.text}${value.taskId === undefined ? '' : `\n(task ${value.taskId})`}`,
|
|
160
|
+
}],
|
|
161
|
+
},
|
|
162
|
+
runtime: 'a2a_send',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
|
|
166
|
+
name: 'a2a_stream',
|
|
167
|
+
description:
|
|
168
|
+
'Stream a task to a remote A2A agent. Progress arrives as it happens and the answer arrives at the end. Use it for anything that may take minutes, so the user sees it working instead of a silent wait.',
|
|
169
|
+
parameters: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
additionalProperties: false,
|
|
172
|
+
properties: {
|
|
173
|
+
agent: { type: 'string', description: 'A registered agent name or a raw A2A base URL.' },
|
|
174
|
+
skill: { type: 'string', description: 'Skill id from a2a_discover.' },
|
|
175
|
+
message: { type: 'string', description: 'The task text.' },
|
|
176
|
+
data: { type: 'string', description: 'Optional JSON object, as a string, for skills that need structured input.' },
|
|
177
|
+
},
|
|
178
|
+
required: ['agent', 'message'],
|
|
179
|
+
},
|
|
180
|
+
output: {
|
|
181
|
+
schema: {
|
|
182
|
+
type: 'object',
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
properties: {
|
|
185
|
+
state: { type: 'string' },
|
|
186
|
+
text: { type: 'string' },
|
|
187
|
+
events: { type: 'integer' },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
render: (args, value) => [{
|
|
191
|
+
type: 'text',
|
|
192
|
+
text: `[${value.state}] ${value.text}\n(${value.events} stream event(s) received)`,
|
|
193
|
+
}],
|
|
194
|
+
},
|
|
195
|
+
runtime: 'a2a_stream',
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
{
|
|
199
|
+
name: 'a2a_person_send',
|
|
200
|
+
description:
|
|
201
|
+
'Send ONE message to ONE person — a group invitation, a join notice, or a welcome. This is a doorbell, not a chat: sustained private conversation belongs in a group, and there is no message kind for it. Use it when a colleague should be pulled into a group rather than handed a link to paste. The result says whether they are online and gives the message id; delivery is confirmed separately, because an HTTP call returning is not evidence that anybody received it.',
|
|
202
|
+
parameters: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
additionalProperties: false,
|
|
205
|
+
properties: {
|
|
206
|
+
to: { type: 'string', description: 'personId of the recipient. Get it from a2a_group_info\'s roster or a colleague card.' },
|
|
207
|
+
kind: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
enum: ['group-invite', 'join-notice', 'welcome'],
|
|
210
|
+
description: 'group-invite mints a fresh single-use link for this one person. join-notice tells an owner somebody joined. welcome greets a new member.',
|
|
211
|
+
},
|
|
212
|
+
groupId: { type: 'string', description: 'The group the message is about. Required for group-invite.' },
|
|
213
|
+
text: { type: 'string', description: 'Optional line shown to the recipient. A group-invite ignores it and shows the group instead.' },
|
|
214
|
+
},
|
|
215
|
+
required: ['to', 'kind'],
|
|
216
|
+
},
|
|
217
|
+
output: {
|
|
218
|
+
schema: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
additionalProperties: false,
|
|
221
|
+
properties: {
|
|
222
|
+
dmId: { type: 'string' },
|
|
223
|
+
online: { type: 'boolean' },
|
|
224
|
+
kind: { type: 'string' },
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
render: (args, value) => [{
|
|
228
|
+
type: 'text',
|
|
229
|
+
text: value.online === true
|
|
230
|
+
? `Queued and the recipient is online (dmId ${value.dmId}). Delivery is confirmed when their Session acknowledges it.`
|
|
231
|
+
: `Queued (dmId ${value.dmId}); the recipient is offline and will see it when they reconnect.`,
|
|
232
|
+
}],
|
|
233
|
+
},
|
|
234
|
+
runtime: 'a2a_person_send',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'a2a_person_inbox',
|
|
238
|
+
description:
|
|
239
|
+
'Messages waiting for me — invitations to groups, notices, welcomes. Read this on startup and when asked "anything waiting?". Each message carries what to act on: a group invitation includes a ready invite link, so joining does not require pasting anything.',
|
|
240
|
+
parameters: {
|
|
241
|
+
type: 'object',
|
|
242
|
+
additionalProperties: false,
|
|
243
|
+
properties: {
|
|
244
|
+
since: { type: 'number', description: 'Only messages created after this epoch-ms. Omit for everything current.' },
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
output: {
|
|
248
|
+
schema: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
additionalProperties: false,
|
|
251
|
+
properties: {
|
|
252
|
+
messages: { type: 'array', items: { type: 'object' } },
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
render: (args, value) => {
|
|
256
|
+
const list = Array.isArray(value.messages) ? value.messages : [];
|
|
257
|
+
return [{
|
|
258
|
+
type: 'text',
|
|
259
|
+
text: list.length === 0
|
|
260
|
+
? 'Nothing waiting.'
|
|
261
|
+
: list.map((entry) => `- [${entry.kind}] ${entry.text}`).join('\n'),
|
|
262
|
+
}];
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
runtime: 'a2a_person_inbox',
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'a2a_person_ack',
|
|
269
|
+
description:
|
|
270
|
+
'Confirm that messages actually arrived. Called automatically after reading the inbox; without it the sender never learns whether the message was delivered, and a messenger that reports success for undelivered messages loses trust immediately.',
|
|
271
|
+
parameters: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
additionalProperties: false,
|
|
274
|
+
properties: {
|
|
275
|
+
dmIds: { type: 'array', items: { type: 'string' }, description: 'Message ids to confirm.' },
|
|
276
|
+
},
|
|
277
|
+
required: ['dmIds'],
|
|
278
|
+
},
|
|
279
|
+
output: {
|
|
280
|
+
schema: {
|
|
281
|
+
type: 'object',
|
|
282
|
+
additionalProperties: false,
|
|
283
|
+
properties: {
|
|
284
|
+
acked: { type: 'number' },
|
|
285
|
+
unknown: { type: 'array', items: { type: 'string' } },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
render: (args, value) => [{
|
|
289
|
+
type: 'text',
|
|
290
|
+
text: `Confirmed ${value.acked} message(s)`
|
|
291
|
+
+ (Array.isArray(value.unknown) && value.unknown.length > 0
|
|
292
|
+
? `; ${value.unknown.length} were not mine and were ignored.`
|
|
293
|
+
: '.'),
|
|
294
|
+
}],
|
|
295
|
+
},
|
|
296
|
+
runtime: 'a2a_person_ack',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'a2a_pending',
|
|
300
|
+
description:
|
|
301
|
+
'Is anything waiting for me? One call that answers it: unread group messages plus one-to-one invitations and notices, with counts and a one-line summary each. Call this at the START of a turn, or every few minutes while working — this server cannot push to you, so polling this is how you notice a colleague. Say "nothing waiting" when it returns empty rather than silently moving on.',
|
|
302
|
+
parameters: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
additionalProperties: false,
|
|
305
|
+
properties: {
|
|
306
|
+
includeGroups: {
|
|
307
|
+
type: 'boolean',
|
|
308
|
+
description: 'Also report groups with unread messages. Defaults to true; pass false for direct messages only.',
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
output: {
|
|
313
|
+
schema: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
additionalProperties: false,
|
|
316
|
+
properties: {
|
|
317
|
+
total: { type: 'number' },
|
|
318
|
+
directMessages: { type: 'array', items: { type: 'object' } },
|
|
319
|
+
groups: { type: 'array', items: { type: 'object' } },
|
|
320
|
+
summary: { type: 'string' },
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
render: (args, value) => [{
|
|
324
|
+
type: 'text',
|
|
325
|
+
text: value.total === 0
|
|
326
|
+
? 'Nothing waiting.'
|
|
327
|
+
: (value.directMessages.length > 0
|
|
328
|
+
? `${value.directMessages.length} direct message(s):\n`
|
|
329
|
+
+ value.directMessages.map((entry) => ` - [${entry.kind}] ${entry.text}`).join('\n')
|
|
330
|
+
: '')
|
|
331
|
+
+ (value.groups.length > 0
|
|
332
|
+
? `\n${value.groups.length} group(s) with unread messages:\n`
|
|
333
|
+
+ value.groups.map((entry) => ` - ${entry.name}: ${entry.unread} new`).join('\n')
|
|
334
|
+
: ''),
|
|
335
|
+
}],
|
|
336
|
+
},
|
|
337
|
+
runtime: 'a2a_pending',
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
|
|
341
|
+
name: 'a2a_group_create',
|
|
342
|
+
description:
|
|
343
|
+
'Create a collaboration group on the group host and get the group id plus a one-time owner key. The group lives on the HOST (a standalone A2A server), not on this machine, so members reach it through the gateway. The calling Session becomes the owner. Use it when several people or Sessions should work on one thing together.',
|
|
344
|
+
parameters: {
|
|
345
|
+
type: 'object',
|
|
346
|
+
additionalProperties: false,
|
|
347
|
+
properties: {
|
|
348
|
+
name: { type: 'string', description: 'Group name, shown to members.' },
|
|
349
|
+
topic: { type: 'string', description: 'One line about what the group is for.' },
|
|
350
|
+
visibility: {
|
|
351
|
+
type: 'string',
|
|
352
|
+
enum: ['public', 'unlisted'],
|
|
353
|
+
description: 'public: listed in the channel directory. unlisted: absent from it entirely, joinable by invite link only. Defaults to public.',
|
|
354
|
+
},
|
|
355
|
+
agent: { type: 'string', description: 'Host to create the group on: a registered agent name or a raw URL. Defaults to the configured group host.' },
|
|
356
|
+
},
|
|
357
|
+
required: ['name'],
|
|
358
|
+
},
|
|
359
|
+
output: {
|
|
360
|
+
schema: {
|
|
361
|
+
type: 'object',
|
|
362
|
+
additionalProperties: false,
|
|
363
|
+
properties: {
|
|
364
|
+
groupId: { type: 'string' },
|
|
365
|
+
name: { type: 'string' },
|
|
366
|
+
personId: { type: 'string' },
|
|
367
|
+
ownerKey: { type: 'string' },
|
|
368
|
+
agentName: { type: 'string' },
|
|
369
|
+
remote: { type: 'boolean' },
|
|
370
|
+
nextSteps: { type: 'string' },
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
render: (args, value) => [{
|
|
374
|
+
type: 'text',
|
|
375
|
+
text: `Group "${value.name}" created on ${value.remote === true ? 'the group host' : 'this instance'}.
|
|
376
|
+
|
|
377
|
+
`
|
|
378
|
+
+ `groupId : ${value.groupId}
|
|
379
|
+
`
|
|
380
|
+
+ `personId : ${value.personId}
|
|
381
|
+
`
|
|
382
|
+
+ `ownerKey : ${value.ownerKey}
|
|
383
|
+
|
|
384
|
+
`
|
|
385
|
+
+ value.nextSteps,
|
|
386
|
+
}],
|
|
387
|
+
},
|
|
388
|
+
runtime: 'a2a_group_create',
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
|
|
392
|
+
name: 'a2a_group_invite',
|
|
393
|
+
description:
|
|
394
|
+
'Mint a group invite: a link plus a message ready to paste into an IM. The link carries a token the owner can revoke at any time; by default it expires in 7 days, and it has no use counter so it works for everyone you send it to. Only the group owner may mint one. Give the user the prompt and let them decide where to send it.',
|
|
395
|
+
parameters: {
|
|
396
|
+
type: 'object',
|
|
397
|
+
additionalProperties: false,
|
|
398
|
+
properties: {
|
|
399
|
+
groupId: { type: 'string', description: 'Group id from a2a_group_create or a2a_groups.' },
|
|
400
|
+
label: { type: 'string', description: 'Who this invite is for, e.g. "for 张工". Shown in the invite list.' },
|
|
401
|
+
expiresInHours: { type: 'integer', description: 'How long the link stays valid. Defaults to the configured value.' },
|
|
402
|
+
},
|
|
403
|
+
required: ['groupId'],
|
|
404
|
+
},
|
|
405
|
+
output: {
|
|
406
|
+
schema: {
|
|
407
|
+
type: 'object',
|
|
408
|
+
additionalProperties: false,
|
|
409
|
+
properties: {
|
|
410
|
+
link: { type: 'string' },
|
|
411
|
+
prompt: { type: 'string' },
|
|
412
|
+
expiresAt: { type: 'string' },
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
render: (args, value) => [{
|
|
416
|
+
type: 'text',
|
|
417
|
+
text: `${value.neverExpires === true ? 'Invite created (never expires).' : `Invite created (valid until ${value.expiresAt}).`}\n\n`
|
|
418
|
+
+ 'Paste this into the IM:\n---\n'
|
|
419
|
+
+ value.prompt
|
|
420
|
+
+ '\n---\n\nLink on its own:\n'
|
|
421
|
+
+ value.link,
|
|
422
|
+
}],
|
|
423
|
+
},
|
|
424
|
+
runtime: 'a2a_group_invite',
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
|
|
428
|
+
name: 'a2a_group_join',
|
|
429
|
+
description:
|
|
430
|
+
'Join a group from an invite link (or a raw groupId plus token). No password is involved: the token IS the credential. Works for a group on this instance and for one behind the gateway — a remote group is joined over A2A. After joining, this Session receives the group\'s messages.',
|
|
431
|
+
parameters: {
|
|
432
|
+
type: 'object',
|
|
433
|
+
additionalProperties: false,
|
|
434
|
+
properties: {
|
|
435
|
+
link: { type: 'string', description: 'The agent-a2a://group?... invite link.' },
|
|
436
|
+
groupId: { type: 'string', description: 'Group id, when joining without a link.' },
|
|
437
|
+
token: { type: 'string', description: 'Invite token, when joining without a link.' },
|
|
438
|
+
displayName: { type: 'string', description: 'How this member appears to the others.' },
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
output: {
|
|
442
|
+
schema: {
|
|
443
|
+
type: 'object',
|
|
444
|
+
additionalProperties: false,
|
|
445
|
+
properties: {
|
|
446
|
+
groupId: { type: 'string' },
|
|
447
|
+
groupName: { type: 'string' },
|
|
448
|
+
personId: { type: 'string' },
|
|
449
|
+
remote: { type: 'boolean' },
|
|
450
|
+
recentMessages: { type: 'integer' },
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
render: (args, value) => [{
|
|
454
|
+
type: 'text',
|
|
455
|
+
text: `Joined "${value.groupName}" (${value.groupId})${value.remote ? ' over A2A' : ' locally'}.\n`
|
|
456
|
+
+ `personId: ${value.personId}\n`
|
|
457
|
+
+ (value.recentMessages === 0
|
|
458
|
+
? 'The group has no messages yet.'
|
|
459
|
+
: `${value.recentMessages} recent message(s) were fetched.`)
|
|
460
|
+
+ '\nNew posts will arrive in this Session as task messages.',
|
|
461
|
+
}],
|
|
462
|
+
},
|
|
463
|
+
runtime: 'a2a_group_join',
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
|
|
467
|
+
name: 'a2a_group_post',
|
|
468
|
+
description:
|
|
469
|
+
'Post a message to a group. Every member is woken by it, so post what a member should act on, not a running commentary. Read a2a_group_read first when you need to know what was already said.',
|
|
470
|
+
parameters: {
|
|
471
|
+
type: 'object',
|
|
472
|
+
additionalProperties: false,
|
|
473
|
+
properties: {
|
|
474
|
+
groupId: { type: 'string', description: 'Group id.' },
|
|
475
|
+
mentions: {
|
|
476
|
+
type: 'array',
|
|
477
|
+
items: { type: 'string' },
|
|
478
|
+
description: 'Who should ACT on this: member ids, display names that resolve to exactly one member, or ["@all"] for everyone. Those members are woken; everyone else only sees an unread count. Omit it for ordinary progress.',
|
|
479
|
+
},
|
|
480
|
+
message: { type: 'string', description: 'The text to post.' },
|
|
481
|
+
},
|
|
482
|
+
required: ['groupId', 'message'],
|
|
483
|
+
},
|
|
484
|
+
output: {
|
|
485
|
+
schema: {
|
|
486
|
+
type: 'object',
|
|
487
|
+
additionalProperties: false,
|
|
488
|
+
properties: { seq: { type: 'integer' }, groupId: { type: 'string' } },
|
|
489
|
+
},
|
|
490
|
+
render: (args, value) => [{ type: 'text', text: `Posted to ${value.groupId} at seq ${value.seq}.` }],
|
|
491
|
+
},
|
|
492
|
+
runtime: 'a2a_group_post',
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
|
|
496
|
+
name: 'a2a_group_role',
|
|
497
|
+
description:
|
|
498
|
+
'Say what you do in ONE group, and change it whenever it changes. This is self-description, not a permission — it grants nothing, and nobody can set it for you. Use it when someone takes on a different hat in a project (a developer who is also acting as product), so the group sees the current role rather than the job title. Omit `about` or `tags` to leave that part unchanged; pass an empty string / empty array to clear it.',
|
|
499
|
+
parameters: {
|
|
500
|
+
type: 'object',
|
|
501
|
+
additionalProperties: false,
|
|
502
|
+
properties: {
|
|
503
|
+
groupId: { type: 'string', description: 'Group id.' },
|
|
504
|
+
about: { type: 'string', description: 'One sentence: what you do in this group. Max 120 characters. Empty string clears it.' },
|
|
505
|
+
tags: {
|
|
506
|
+
type: 'array',
|
|
507
|
+
items: { type: 'string' },
|
|
508
|
+
description: 'Up to 5 short labels describing your role here, e.g. ["产品","验收"]. An empty array clears them.',
|
|
509
|
+
},
|
|
510
|
+
displayName: { type: 'string', description: 'Optional: how you appear in this group, if you want to change it too.' },
|
|
511
|
+
},
|
|
512
|
+
required: ['groupId'],
|
|
513
|
+
},
|
|
514
|
+
output: {
|
|
515
|
+
schema: {
|
|
516
|
+
type: 'object',
|
|
517
|
+
additionalProperties: false,
|
|
518
|
+
properties: {
|
|
519
|
+
groupId: { type: 'string' },
|
|
520
|
+
about: { type: 'string' },
|
|
521
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
render: (args, value) => [{
|
|
525
|
+
type: 'text',
|
|
526
|
+
text: `Your role in this group is now:\n`
|
|
527
|
+
+ ` about: ${value.about === '' ? '(not set)' : value.about}\n`
|
|
528
|
+
+ ` tags : ${(value.tags ?? []).join('、') || '(none)'}\n\n`
|
|
529
|
+
+ 'This is self-description only — it grants no permission, and it only '
|
|
530
|
+
+ 'changes YOUR role, not anyone else\'s.',
|
|
531
|
+
}],
|
|
532
|
+
},
|
|
533
|
+
runtime: 'a2a_group_role',
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
|
|
537
|
+
name: 'a2a_group_info',
|
|
538
|
+
description:
|
|
539
|
+
'Fetch one group name, topic, member count and your role in it. Use it after joining a remote group so the panel can show a real name instead of an id, or to check whether anybody is online.',
|
|
540
|
+
parameters: {
|
|
541
|
+
type: 'object',
|
|
542
|
+
additionalProperties: false,
|
|
543
|
+
properties: {
|
|
544
|
+
groupId: { type: 'string', description: 'Group id.' },
|
|
545
|
+
refresh: { type: 'boolean', description: 'Re-fetch from the host even if a cached value exists. Defaults to true for a remote group.' },
|
|
546
|
+
},
|
|
547
|
+
required: ['groupId'],
|
|
548
|
+
},
|
|
549
|
+
output: {
|
|
550
|
+
schema: {
|
|
551
|
+
type: 'object',
|
|
552
|
+
additionalProperties: false,
|
|
553
|
+
properties: {
|
|
554
|
+
groupId: { type: 'string' },
|
|
555
|
+
name: { type: 'string' },
|
|
556
|
+
topic: { type: 'string' },
|
|
557
|
+
memberCount: { type: 'integer' },
|
|
558
|
+
onlineCount: { type: 'integer' },
|
|
559
|
+
lastSeq: { type: 'integer' },
|
|
560
|
+
isOwner: { type: 'boolean' },
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
render: (args, value) => [{
|
|
564
|
+
type: 'text',
|
|
565
|
+
text: `${value.name}`
|
|
566
|
+
+ (value.topic === '' ? '' : ` — ${value.topic}`)
|
|
567
|
+
+ `\n${value.memberCount} 人`
|
|
568
|
+
+ (value.onlineCount > 0 ? `,${value.onlineCount} 人在线` : '')
|
|
569
|
+
+ `\n我的角色:${value.isOwner === true ? '群主' : '成员'}`,
|
|
570
|
+
}],
|
|
571
|
+
},
|
|
572
|
+
runtime: 'a2a_group_info',
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
|
|
576
|
+
name: 'a2a_group_read',
|
|
577
|
+
description:
|
|
578
|
+
'Read recent messages in a group. Pass sinceSeq to get only what is new — that is what a reconnect needs. Read-only: it never wakes a Session.',
|
|
579
|
+
parameters: {
|
|
580
|
+
type: 'object',
|
|
581
|
+
additionalProperties: false,
|
|
582
|
+
properties: {
|
|
583
|
+
groupId: { type: 'string', description: 'Group id.' },
|
|
584
|
+
sinceSeq: { type: 'integer', description: 'Return only messages with a sequence number above this.' },
|
|
585
|
+
limit: { type: 'integer', description: 'Maximum number of messages to return.' },
|
|
586
|
+
},
|
|
587
|
+
required: ['groupId'],
|
|
588
|
+
},
|
|
589
|
+
output: {
|
|
590
|
+
schema: {
|
|
591
|
+
type: 'object',
|
|
592
|
+
additionalProperties: false,
|
|
593
|
+
properties: {
|
|
594
|
+
lastSeq: { type: 'integer' },
|
|
595
|
+
entries: {
|
|
596
|
+
type: 'array',
|
|
597
|
+
items: {
|
|
598
|
+
type: 'object',
|
|
599
|
+
additionalProperties: false,
|
|
600
|
+
properties: {
|
|
601
|
+
seq: { type: 'integer' },
|
|
602
|
+
from: { type: 'string' },
|
|
603
|
+
text: { type: 'string' },
|
|
604
|
+
at: { type: 'string' },
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
render: (args, value) => [{
|
|
611
|
+
type: 'text',
|
|
612
|
+
text: value.entries.length === 0
|
|
613
|
+
? 'No new messages.'
|
|
614
|
+
: value.entries.map((entry) => `#${entry.seq} ${entry.from}: ${entry.text}`).join('\n'),
|
|
615
|
+
}],
|
|
616
|
+
},
|
|
617
|
+
runtime: 'a2a_group_read',
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
|
|
621
|
+
name: 'a2a_group_directory',
|
|
622
|
+
description:
|
|
623
|
+
'Browse the groups a HOST offers, before joining any of them: name, topic, join policy, member count, last activity. Needs no membership, so it is the right tool when someone asks "what groups are there" or "is there anything about X". It never returns member names or message content — for those, join and use a2a_groups / a2a_group_read. Groups the host marked unlisted do not appear at all.',
|
|
624
|
+
parameters: {
|
|
625
|
+
type: 'object',
|
|
626
|
+
additionalProperties: false,
|
|
627
|
+
properties: {
|
|
628
|
+
agent: {
|
|
629
|
+
type: 'string',
|
|
630
|
+
description: 'Which host to browse: a registered agent name from a2a_agents, or a raw A2A base URL. Defaults to the configured group host.',
|
|
631
|
+
},
|
|
632
|
+
query: { type: 'string', description: 'Optional filter over group name and topic.' },
|
|
633
|
+
limit: { type: 'integer', description: 'Maximum groups to return. Defaults to the host\'s page size.' },
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
output: {
|
|
637
|
+
schema: {
|
|
638
|
+
type: 'object',
|
|
639
|
+
additionalProperties: false,
|
|
640
|
+
properties: {
|
|
641
|
+
total: { type: 'integer' },
|
|
642
|
+
hasMore: { type: 'boolean' },
|
|
643
|
+
groups: {
|
|
644
|
+
type: 'array',
|
|
645
|
+
items: {
|
|
646
|
+
type: 'object',
|
|
647
|
+
additionalProperties: false,
|
|
648
|
+
properties: {
|
|
649
|
+
groupId: { type: 'string' },
|
|
650
|
+
name: { type: 'string' },
|
|
651
|
+
topic: { type: 'string' },
|
|
652
|
+
memberCount: { type: 'integer' },
|
|
653
|
+
onlineCount: { type: 'integer' },
|
|
654
|
+
lastActivityAt: { type: 'string' },
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
render: (args, value) => [{
|
|
661
|
+
type: 'text',
|
|
662
|
+
text: (value.groups ?? []).length === 0
|
|
663
|
+
? 'That host lists no public groups.'
|
|
664
|
+
: `${value.total} group(s)${value.hasMore === true ? ', showing the first page' : ''}:\n`
|
|
665
|
+
+ value.groups.map((group) => {
|
|
666
|
+
const online = group.onlineCount > 0 ? `,${group.onlineCount} 人在线` : '';
|
|
667
|
+
// No join policy in this line: a group has no admission mode to advertise any
|
|
668
|
+
// more. Anyone may ASK to join, and an invite link needs the owner to mint one.
|
|
669
|
+
return `- ${group.name}(${group.memberCount} 人${online})`
|
|
670
|
+
+ (group.topic === '' ? '' : `\n ${group.topic}`)
|
|
671
|
+
+ `\n id: ${group.groupId}`;
|
|
672
|
+
}).join('\n')
|
|
673
|
+
+ '\n\n要加入某个群:用 a2a_group_request 申请(群主会看到),或向群主要一个邀请链接。',
|
|
674
|
+
}],
|
|
675
|
+
},
|
|
676
|
+
runtime: 'a2a_group_directory',
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
|
|
680
|
+
name: 'a2a_groups',
|
|
681
|
+
description:
|
|
682
|
+
'List the groups this instance hosts or has joined, with member count and the last message. Use it to answer "which groups am I in" instead of reading transcripts.',
|
|
683
|
+
parameters: {
|
|
684
|
+
type: 'object',
|
|
685
|
+
additionalProperties: false,
|
|
686
|
+
properties: {
|
|
687
|
+
groupId: { type: 'string', description: 'Optional: only this group.' },
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
output: {
|
|
691
|
+
schema: {
|
|
692
|
+
type: 'object',
|
|
693
|
+
additionalProperties: false,
|
|
694
|
+
properties: {
|
|
695
|
+
groups: {
|
|
696
|
+
type: 'array',
|
|
697
|
+
items: {
|
|
698
|
+
type: 'object',
|
|
699
|
+
additionalProperties: false,
|
|
700
|
+
properties: {
|
|
701
|
+
groupId: { type: 'string' },
|
|
702
|
+
name: { type: 'string' },
|
|
703
|
+
role: { type: 'string' },
|
|
704
|
+
members: { type: 'integer' },
|
|
705
|
+
lastSeq: { type: 'integer' },
|
|
706
|
+
lastMessage: { type: 'string' },
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
render: (args, value) => [{
|
|
713
|
+
type: 'text',
|
|
714
|
+
text: value.groups.length === 0
|
|
715
|
+
? 'No groups yet. Create one with a2a_group_create, or join one with a2a_group_join.'
|
|
716
|
+
: value.groups.map((group) => (
|
|
717
|
+
`- ${group.name} (${group.groupId}) — ${group.role}, ${group.members} member(s), last seq ${group.lastSeq}`
|
|
718
|
+
+ (group.lastMessage === '' ? '' : `\n "${group.lastMessage}"`)
|
|
719
|
+
)).join('\n'),
|
|
720
|
+
}],
|
|
721
|
+
},
|
|
722
|
+
runtime: 'a2a_groups',
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
|
|
726
|
+
name: 'a2a_group_inbox',
|
|
727
|
+
description:
|
|
728
|
+
'Turn this instance\'s group subscription on or off, or report its state. While on, a group post wakes this Session as a task message; while off, the group is readable but silent. Use status to diagnose "why did I not get the message".',
|
|
729
|
+
parameters: {
|
|
730
|
+
type: 'object',
|
|
731
|
+
additionalProperties: false,
|
|
732
|
+
properties: {
|
|
733
|
+
action: { type: 'string', enum: ['on', 'off', 'status'], description: 'What to do.' },
|
|
734
|
+
groupId: { type: 'string', description: 'Optional: apply to one group only.' },
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
output: {
|
|
738
|
+
schema: {
|
|
739
|
+
type: 'object',
|
|
740
|
+
additionalProperties: false,
|
|
741
|
+
properties: {
|
|
742
|
+
enabled: { type: 'boolean' },
|
|
743
|
+
subscriptions: {
|
|
744
|
+
type: 'array',
|
|
745
|
+
items: {
|
|
746
|
+
type: 'object',
|
|
747
|
+
additionalProperties: false,
|
|
748
|
+
properties: {
|
|
749
|
+
groupId: { type: 'string' },
|
|
750
|
+
active: { type: 'boolean' },
|
|
751
|
+
lastSeq: { type: 'integer' },
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
render: (args, value) => [{
|
|
758
|
+
type: 'text',
|
|
759
|
+
text: `group inbox: ${value.enabled ? 'on' : 'off'}\n`
|
|
760
|
+
+ ((value.subscriptions ?? []).length === 0
|
|
761
|
+
? '(no subscriptions)'
|
|
762
|
+
: value.subscriptions
|
|
763
|
+
.map((item) => `- ${item.groupId}: ${item.active ? 'subscribed' : 'idle'}, last seq ${item.lastSeq}`)
|
|
764
|
+
.join('\n')),
|
|
765
|
+
}],
|
|
766
|
+
},
|
|
767
|
+
runtime: 'a2a_group_inbox',
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
|
|
771
|
+
name: 'a2a_group_request',
|
|
772
|
+
description:
|
|
773
|
+
'Apply to join a group you found with a2a_group_directory. Needs no invitation and no password — you are asking, not entering. Returns a requestId (a queue index, safe to share) and a claimKey (SECRET: keep it, it collects the membership once the group owner approves). The owner decides on their own schedule; then use a2a_group_claim.',
|
|
774
|
+
parameters: {
|
|
775
|
+
type: 'object',
|
|
776
|
+
additionalProperties: false,
|
|
777
|
+
properties: {
|
|
778
|
+
groupId: { type: 'string', description: 'Group id from a2a_group_directory.' },
|
|
779
|
+
displayName: { type: 'string', description: 'Your name as the owner will see it.' },
|
|
780
|
+
reason: { type: 'string', description: 'One line about why you want in, so the owner can judge. Optional but strongly recommended.' },
|
|
781
|
+
agent: { type: 'string', description: 'Host to apply to: a registered agent name or a raw URL. Defaults to the configured group host.' },
|
|
782
|
+
},
|
|
783
|
+
required: ['groupId', 'displayName'],
|
|
784
|
+
},
|
|
785
|
+
output: {
|
|
786
|
+
schema: {
|
|
787
|
+
type: 'object',
|
|
788
|
+
additionalProperties: false,
|
|
789
|
+
properties: {
|
|
790
|
+
requestId: { type: 'string' },
|
|
791
|
+
claimKey: { type: 'string' },
|
|
792
|
+
groupName: { type: 'string' },
|
|
793
|
+
expiresAt: { type: 'string' },
|
|
794
|
+
duplicate: { type: 'boolean' },
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
render: (args, value) => [{
|
|
798
|
+
type: 'text',
|
|
799
|
+
text: value.duplicate === true
|
|
800
|
+
? `You already have a pending request for「${value.groupName}」(${value.requestId}). Wait for the owner.
|
|
801
|
+
`
|
|
802
|
+
+ '(The claimKey from your FIRST request is the one that works; this call does not issue a new one.)'
|
|
803
|
+
: `Applied to join「${value.groupName}」.
|
|
804
|
+
|
|
805
|
+
`
|
|
806
|
+
+ `requestId : ${value.requestId}
|
|
807
|
+
`
|
|
808
|
+
+ `claimKey : ${value.claimKey}
|
|
809
|
+
|
|
810
|
+
`
|
|
811
|
+
+ 'Keep the claimKey — it is shown once and is what collects your membership.\n'
|
|
812
|
+
+ `Valid until ${value.expiresAt}.`,
|
|
813
|
+
}],
|
|
814
|
+
},
|
|
815
|
+
runtime: 'a2a_group_request',
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
|
|
819
|
+
name: 'a2a_group_requests',
|
|
820
|
+
description:
|
|
821
|
+
'List the applications waiting for YOUR decision. Owner only: the queue holds other people\'s names and stated reasons. Use a2a_group_decide to answer one.',
|
|
822
|
+
parameters: {
|
|
823
|
+
type: 'object',
|
|
824
|
+
additionalProperties: false,
|
|
825
|
+
properties: {
|
|
826
|
+
groupId: { type: 'string', description: 'Group id.' },
|
|
827
|
+
status: { type: 'string', enum: ['pending', 'approved', 'rejected', 'claimed', 'all'], description: 'Which requests to list. Defaults to pending.' },
|
|
828
|
+
},
|
|
829
|
+
required: ['groupId'],
|
|
830
|
+
},
|
|
831
|
+
output: {
|
|
832
|
+
schema: {
|
|
833
|
+
type: 'object',
|
|
834
|
+
additionalProperties: false,
|
|
835
|
+
properties: {
|
|
836
|
+
count: { type: 'integer' },
|
|
837
|
+
requests: {
|
|
838
|
+
type: 'array',
|
|
839
|
+
items: {
|
|
840
|
+
type: 'object',
|
|
841
|
+
additionalProperties: false,
|
|
842
|
+
properties: {
|
|
843
|
+
requestId: { type: 'string' },
|
|
844
|
+
displayName: { type: 'string' },
|
|
845
|
+
reason: { type: 'string' },
|
|
846
|
+
status: { type: 'string' },
|
|
847
|
+
expiresAt: { type: 'string' },
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
render: (args, value) => [{
|
|
854
|
+
type: 'text',
|
|
855
|
+
text: (value.requests ?? []).length === 0
|
|
856
|
+
? 'No requests to review.'
|
|
857
|
+
: `${value.count} request(s):\n`
|
|
858
|
+
+ value.requests.map((entry) => `- ${entry.displayName} [${entry.status}] (${entry.requestId})`
|
|
859
|
+
+ (entry.reason === '' ? '' : `\n "${entry.reason}"`)
|
|
860
|
+
+ `\n expires ${entry.expiresAt}`).join('\n'),
|
|
861
|
+
}],
|
|
862
|
+
},
|
|
863
|
+
runtime: 'a2a_group_requests',
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
|
|
867
|
+
name: 'a2a_group_decide',
|
|
868
|
+
description:
|
|
869
|
+
'Approve or reject one join request. Owner only. Approving does NOT create the membership: the applicant collects it with the claimKey they already hold — that is what keeps "one person vouched for one person" true.',
|
|
870
|
+
parameters: {
|
|
871
|
+
type: 'object',
|
|
872
|
+
additionalProperties: false,
|
|
873
|
+
properties: {
|
|
874
|
+
groupId: { type: 'string', description: 'Group id.' },
|
|
875
|
+
requestId: { type: 'string', description: 'Request id from a2a_group_requests.' },
|
|
876
|
+
approve: { type: 'boolean', description: 'true to approve, false to reject. Defaults to true.' },
|
|
877
|
+
},
|
|
878
|
+
required: ['groupId', 'requestId'],
|
|
879
|
+
},
|
|
880
|
+
output: {
|
|
881
|
+
schema: {
|
|
882
|
+
type: 'object',
|
|
883
|
+
additionalProperties: false,
|
|
884
|
+
properties: {
|
|
885
|
+
requestId: { type: 'string' },
|
|
886
|
+
status: { type: 'string' },
|
|
887
|
+
displayName: { type: 'string' },
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
render: (args, value) => [{
|
|
891
|
+
type: 'text',
|
|
892
|
+
text: `${value.status === 'approved' ? 'Approved' : 'Rejected'} ${value.displayName}'s request.`
|
|
893
|
+
+ (value.status === 'approved'
|
|
894
|
+
? '\nThey collect the membership with their own claimKey — you do not need to send an invite.'
|
|
895
|
+
: ''),
|
|
896
|
+
}],
|
|
897
|
+
},
|
|
898
|
+
runtime: 'a2a_group_decide',
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
|
|
902
|
+
name: 'a2a_group_claim',
|
|
903
|
+
description:
|
|
904
|
+
'Collect an approved membership using the claimKey you got when you applied. Needs NO group password: the owner already vouched for you. The claimKey is single-use and only you hold it.',
|
|
905
|
+
parameters: {
|
|
906
|
+
type: 'object',
|
|
907
|
+
additionalProperties: false,
|
|
908
|
+
properties: {
|
|
909
|
+
groupId: { type: 'string', description: 'Group id you applied to.' },
|
|
910
|
+
requestId: { type: 'string', description: 'requestId from a2a_group_request.' },
|
|
911
|
+
claimKey: { type: 'string', description: 'The claimKey returned when you applied.' },
|
|
912
|
+
agent: { type: 'string', description: 'Host the group lives on. Defaults to the configured group host.' },
|
|
913
|
+
displayName: { type: 'string', description: 'Optional: how you appear in the group.' },
|
|
914
|
+
},
|
|
915
|
+
required: ['groupId', 'requestId', 'claimKey'],
|
|
916
|
+
},
|
|
917
|
+
output: {
|
|
918
|
+
schema: {
|
|
919
|
+
type: 'object',
|
|
920
|
+
additionalProperties: false,
|
|
921
|
+
properties: {
|
|
922
|
+
groupId: { type: 'string' },
|
|
923
|
+
groupName: { type: 'string' },
|
|
924
|
+
personId: { type: 'string' },
|
|
925
|
+
remote: { type: 'boolean' },
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
render: (args, value) => [{
|
|
929
|
+
type: 'text',
|
|
930
|
+
text: `Joined「${value.groupName}」(${value.groupId})${value.remote ? ' over A2A' : ' locally'}.
|
|
931
|
+
`
|
|
932
|
+
+ `personId: ${value.personId}
|
|
933
|
+
|
|
934
|
+
`
|
|
935
|
+
+ 'This Session stored the credential; new posts will arrive as task messages.',
|
|
936
|
+
}],
|
|
937
|
+
},
|
|
938
|
+
runtime: 'a2a_group_claim',
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
name: 'a2a_group_remove',
|
|
942
|
+
description:
|
|
943
|
+
'Remove another member from a group. Owner only. Their messages stay in the feed: deleting them would rewrite the conversation, and history is the group record rather than the leaver property. A system line records the removal so the remaining members know what happened. To leave yourself, use a2a_group_leave instead — that hands ownership on.',
|
|
944
|
+
parameters: {
|
|
945
|
+
type: 'object',
|
|
946
|
+
additionalProperties: false,
|
|
947
|
+
properties: {
|
|
948
|
+
groupId: { type: 'string', description: 'Group id.' },
|
|
949
|
+
targetPersonId: { type: 'string', description: 'personId of the person to remove.' },
|
|
950
|
+
reason: { type: 'string', description: 'Optional one-liner for the system entry, e.g. "转岗".' },
|
|
951
|
+
},
|
|
952
|
+
required: ['groupId', 'targetPersonId'],
|
|
953
|
+
},
|
|
954
|
+
output: {
|
|
955
|
+
schema: {
|
|
956
|
+
type: 'object',
|
|
957
|
+
additionalProperties: false,
|
|
958
|
+
properties: {
|
|
959
|
+
groupId: { type: 'string' },
|
|
960
|
+
personId: { type: 'string' },
|
|
961
|
+
displayName: { type: 'string' },
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
render: (args, value) => [{
|
|
965
|
+
type: 'text',
|
|
966
|
+
text: `Removed ${value.displayName} (${value.personId}) from the group.
|
|
967
|
+
|
|
968
|
+
`
|
|
969
|
+
+ 'Their messages remain. They will get "not a member" on their next post, which is how their client '
|
|
970
|
+
+ 'learns to drop its local record.',
|
|
971
|
+
}],
|
|
972
|
+
},
|
|
973
|
+
runtime: 'a2a_group_remove',
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
name: 'a2a_group_rename',
|
|
977
|
+
description:
|
|
978
|
+
'Rename a group, change its topic, or both. Owner only. This is ordinary maintenance for a long-lived group; before it existed the only fix for a wrong name was to create a new group and lose the history.',
|
|
979
|
+
parameters: {
|
|
980
|
+
type: 'object',
|
|
981
|
+
additionalProperties: false,
|
|
982
|
+
properties: {
|
|
983
|
+
groupId: { type: 'string', description: 'Group id.' },
|
|
984
|
+
name: { type: 'string', description: 'New display name. Omit to leave it unchanged.' },
|
|
985
|
+
topic: { type: 'string', description: 'New topic. An empty string clears it. Omit to leave it unchanged.' },
|
|
986
|
+
},
|
|
987
|
+
required: ['groupId'],
|
|
988
|
+
},
|
|
989
|
+
output: {
|
|
990
|
+
schema: {
|
|
991
|
+
type: 'object',
|
|
992
|
+
additionalProperties: false,
|
|
993
|
+
properties: {
|
|
994
|
+
groupId: { type: 'string' },
|
|
995
|
+
name: { type: 'string' },
|
|
996
|
+
topic: { type: 'string' },
|
|
997
|
+
changed: { type: 'array', items: { type: 'string' } },
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
render: (args, value) => [{
|
|
1001
|
+
type: 'text',
|
|
1002
|
+
text: (value.changed ?? []).length === 0
|
|
1003
|
+
? 'Nothing changed: those are the current values.'
|
|
1004
|
+
: `Updated ${value.changed.join(' and ')}.
|
|
1005
|
+
` + `name : ${value.name}` + (value.topic === '' ? '' : `
|
|
1006
|
+
topic: ${value.topic}`),
|
|
1007
|
+
}],
|
|
1008
|
+
},
|
|
1009
|
+
runtime: 'a2a_group_rename',
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
name: 'a2a_group_archive',
|
|
1013
|
+
description:
|
|
1014
|
+
'Archive a group (or reopen it). Owner only. Archived means readable but not writable, and absent from the directory. Deliberately not a delete: a finished project conversation is worth keeping, and reopening an archive by mistake must be possible.',
|
|
1015
|
+
parameters: {
|
|
1016
|
+
type: 'object',
|
|
1017
|
+
additionalProperties: false,
|
|
1018
|
+
properties: {
|
|
1019
|
+
groupId: { type: 'string', description: 'Group id.' },
|
|
1020
|
+
archived: { type: 'boolean', description: 'true to archive (default), false to reopen.' },
|
|
1021
|
+
},
|
|
1022
|
+
required: ['groupId'],
|
|
1023
|
+
},
|
|
1024
|
+
output: {
|
|
1025
|
+
schema: {
|
|
1026
|
+
type: 'object',
|
|
1027
|
+
additionalProperties: false,
|
|
1028
|
+
properties: {
|
|
1029
|
+
groupId: { type: 'string' },
|
|
1030
|
+
archived: { type: 'boolean' },
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
render: (args, value) => [{
|
|
1034
|
+
type: 'text',
|
|
1035
|
+
text: value.archived === true
|
|
1036
|
+
? 'Archived: still readable, no longer writable, hidden from the directory. Call again with archived=false to reopen.'
|
|
1037
|
+
: 'Reopened: writable again and listed in the directory.',
|
|
1038
|
+
}],
|
|
1039
|
+
},
|
|
1040
|
+
runtime: 'a2a_group_archive',
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
|
|
1044
|
+
name: 'a2a_serve',
|
|
1045
|
+
description:
|
|
1046
|
+
'Check whether this DSH instance is reachable as an A2A agent — and if not, which step is missing. Runs a real end-to-end probe through the gateway (card, credential, card rewrite, ping round trip), so it answers "will a colleague\'s invite link actually work" rather than "does the config look right". Use it before asking anyone to join a group, and whenever a caller says they cannot reach this instance. Pass check=false for just the local endpoint and the gateway entry to register.',
|
|
1047
|
+
parameters: {
|
|
1048
|
+
type: 'object',
|
|
1049
|
+
additionalProperties: false,
|
|
1050
|
+
properties: {
|
|
1051
|
+
check: {
|
|
1052
|
+
type: 'boolean',
|
|
1053
|
+
description: 'Run the live reachability probe. Defaults to true. Pass false to only describe the local endpoint and the gateway entry.',
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
},
|
|
1057
|
+
output: {
|
|
1058
|
+
schema: {
|
|
1059
|
+
type: 'object',
|
|
1060
|
+
additionalProperties: false,
|
|
1061
|
+
properties: {
|
|
1062
|
+
enabled: { type: 'boolean' },
|
|
1063
|
+
endpoint: { type: 'string' },
|
|
1064
|
+
cardUrl: { type: 'string' },
|
|
1065
|
+
tokenRequired: { type: 'boolean' },
|
|
1066
|
+
boundSessionId: { type: 'string' },
|
|
1067
|
+
reachable: { type: 'boolean' },
|
|
1068
|
+
steps: {
|
|
1069
|
+
type: 'array',
|
|
1070
|
+
items: {
|
|
1071
|
+
type: 'object',
|
|
1072
|
+
additionalProperties: false,
|
|
1073
|
+
properties: {
|
|
1074
|
+
step: { type: 'string' },
|
|
1075
|
+
ok: { type: 'boolean' },
|
|
1076
|
+
detail: { type: 'string' },
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
},
|
|
1080
|
+
hint: { type: 'string' },
|
|
1081
|
+
gatewayEntry: { type: 'string' },
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
render: (args, value) => {
|
|
1085
|
+
if (!value.enabled) return [{ type: 'text', text: 'The A2A server is disabled in this profile (server.enabled = false).' }];
|
|
1086
|
+
const lines = [
|
|
1087
|
+
`A2A server: ${value.endpoint}`,
|
|
1088
|
+
`card : ${value.cardUrl}`,
|
|
1089
|
+
`auth : ${value.tokenRequired ? 'token required (use it as the gateway agent\'s secret)' : 'loopback only (no token configured)'}`,
|
|
1090
|
+
`session : ${value.boundSessionId || '(not bound yet — the next call binds it)'}`,
|
|
1091
|
+
];
|
|
1092
|
+
if (value.steps !== undefined) {
|
|
1093
|
+
lines.push('', value.reachable ? 'Reachability: OK' : 'Reachability: BLOCKED');
|
|
1094
|
+
for (const step of value.steps) lines.push(` ${step.ok ? 'OK ' : 'FAIL'} ${step.step}: ${step.detail}`);
|
|
1095
|
+
if (value.hint !== undefined && value.hint !== '') lines.push('', value.hint);
|
|
1096
|
+
}
|
|
1097
|
+
if (value.gatewayEntry !== undefined && value.gatewayEntry !== '') {
|
|
1098
|
+
lines.push('', 'Gateway entry for this instance (one entry, all groups):', value.gatewayEntry);
|
|
1099
|
+
}
|
|
1100
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
1101
|
+
},
|
|
1102
|
+
},
|
|
1103
|
+
runtime: 'a2a_serve',
|
|
1104
|
+
},
|
|
1105
|
+
]);
|
|
1106
|
+
|
|
1107
|
+
/** Specs by name, built once. */
|
|
1108
|
+
const BY_NAME = new Map(TOOL_SPECS.map((spec) => [spec.name, spec]));
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* One spec by name.
|
|
1112
|
+
*
|
|
1113
|
+
* @param {string} name - tool name.
|
|
1114
|
+
* @returns {object|undefined} the spec.
|
|
1115
|
+
*/
|
|
1116
|
+
export function specOf(name) {
|
|
1117
|
+
return BY_NAME.get(name);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** Every spec, as an array. */
|
|
1121
|
+
export function allSpecs() {
|
|
1122
|
+
return TOOL_SPECS;
|
|
1123
|
+
}
|