@agentlinkdev/agentlink 0.1.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/LICENSE +21 -0
- package/README.md +73 -0
- package/bin/cli.js +382 -0
- package/openclaw.plugin.json +83 -0
- package/package.json +62 -0
- package/src/channel.ts +91 -0
- package/src/contacts.ts +69 -0
- package/src/index.ts +269 -0
- package/src/invite.ts +99 -0
- package/src/jobs.ts +156 -0
- package/src/mqtt-client.ts +142 -0
- package/src/mqtt-service.ts +157 -0
- package/src/routing.ts +53 -0
- package/src/state.ts +169 -0
- package/src/tools.ts +352 -0
- package/src/types.ts +258 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { v4 as uuid } from "uuid";
|
|
3
|
+
import type { AgentLinkConfig, AgentStatus } from "./types.js";
|
|
4
|
+
import { createEnvelope, TOPICS } from "./types.js";
|
|
5
|
+
import type { StateManager } from "./state.js";
|
|
6
|
+
import type { ContactsManager } from "./contacts.js";
|
|
7
|
+
import type { MqttService } from "./mqtt-service.js";
|
|
8
|
+
import type { InviteManager } from "./invite.js";
|
|
9
|
+
import type { JobManager } from "./jobs.js";
|
|
10
|
+
import type { Logger } from "./mqtt-client.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Tool result helper
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function json(data: Record<string, unknown>) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
19
|
+
details: data,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Publish agent status to group
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
async function publishStatus(
|
|
28
|
+
config: AgentLinkConfig,
|
|
29
|
+
mqtt: MqttService,
|
|
30
|
+
groupId: string,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
const status: AgentStatus = {
|
|
33
|
+
agent_id: config.agent.id,
|
|
34
|
+
owner: config.agent.description?.split("'s")[0] ?? config.agent.id,
|
|
35
|
+
status: "online",
|
|
36
|
+
capabilities: config.agent.capabilities.map((c) => ({
|
|
37
|
+
name: c.name,
|
|
38
|
+
description: c.description ?? c.name,
|
|
39
|
+
input_hint: c.input_hint ?? "",
|
|
40
|
+
})),
|
|
41
|
+
description: config.agent.description,
|
|
42
|
+
ts: new Date().toISOString(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
await mqtt.publish(
|
|
46
|
+
TOPICS.groupStatus(groupId, config.agent.id),
|
|
47
|
+
JSON.stringify(status),
|
|
48
|
+
{ retain: true },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// TypeBox Schemas
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const CoordinateSchema = Type.Object({
|
|
57
|
+
goal: Type.String({ description: "What the user wants to accomplish" }),
|
|
58
|
+
done_when: Type.Optional(
|
|
59
|
+
Type.String({ description: "How to know when this is complete" }),
|
|
60
|
+
),
|
|
61
|
+
participants: Type.Array(Type.String(), {
|
|
62
|
+
description: "Names or agent IDs of people to coordinate with",
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const SubmitJobSchema = Type.Object({
|
|
67
|
+
group_id: Type.String({ description: "The active group/coordination ID" }),
|
|
68
|
+
capability: Type.String({
|
|
69
|
+
description: "The capability to request (e.g. 'check_calendar')",
|
|
70
|
+
}),
|
|
71
|
+
target_agent: Type.Optional(
|
|
72
|
+
Type.String({
|
|
73
|
+
description: "Specific agent ID to target (optional — if omitted, routes by capability)",
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
text: Type.String({
|
|
77
|
+
description: "Natural language description of what you need",
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const InviteAgentSchema = Type.Object({
|
|
82
|
+
group_id: Type.String({ description: "The active group/coordination ID" }),
|
|
83
|
+
name_or_agent_id: Type.String({
|
|
84
|
+
description: "Contact name (e.g. 'Sara') or agent ID (e.g. 'sara-macbook')",
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const JoinGroupSchema = Type.Object({
|
|
89
|
+
invite_code: Type.String({
|
|
90
|
+
description: "The 6-character invite code (e.g. 'AB3X7K')",
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const CompleteSchema = Type.Object({
|
|
95
|
+
group_id: Type.String({ description: "The active group/coordination ID" }),
|
|
96
|
+
summary: Type.String({ description: "Final outcome summary" }),
|
|
97
|
+
success: Type.Optional(
|
|
98
|
+
Type.Boolean({
|
|
99
|
+
description: "Whether the goal was achieved",
|
|
100
|
+
default: true,
|
|
101
|
+
}),
|
|
102
|
+
),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// createTools
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
export function createTools(
|
|
110
|
+
config: AgentLinkConfig,
|
|
111
|
+
state: StateManager,
|
|
112
|
+
contacts: ContactsManager,
|
|
113
|
+
mqtt: MqttService,
|
|
114
|
+
invites: InviteManager,
|
|
115
|
+
jobs: JobManager,
|
|
116
|
+
logger: Logger,
|
|
117
|
+
) {
|
|
118
|
+
function log(msg: string) {
|
|
119
|
+
if (config.outputMode === "debug") {
|
|
120
|
+
logger.info(`[AgentLink] ${msg}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// -----------------------------------------------------------------------
|
|
125
|
+
// agentlink_coordinate
|
|
126
|
+
// -----------------------------------------------------------------------
|
|
127
|
+
const coordinateTool = {
|
|
128
|
+
name: "agentlink_coordinate",
|
|
129
|
+
label: "Coordinate",
|
|
130
|
+
description:
|
|
131
|
+
"Start coordinating with other people's agents. Use this when the user wants to do something that involves other people.",
|
|
132
|
+
parameters: CoordinateSchema,
|
|
133
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
134
|
+
const goal = params.goal as string;
|
|
135
|
+
const doneWhen = (params.done_when as string) ?? `${goal} — completed to user's satisfaction`;
|
|
136
|
+
const participants = params.participants as string[];
|
|
137
|
+
|
|
138
|
+
const resolved = participants.map((p) => ({
|
|
139
|
+
name: p,
|
|
140
|
+
agentId: contacts.resolve(p),
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
const unresolved = resolved.filter((r) => !r.agentId);
|
|
144
|
+
if (unresolved.length > 0) {
|
|
145
|
+
return json({
|
|
146
|
+
error: `Unknown contacts: ${unresolved.map((u) => u.name).join(", ")}. Ask the user for their agent ID, or use agentlink_invite_agent with an agent_id.`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const participantIds = resolved.map((r) => r.agentId!);
|
|
151
|
+
const groupId = uuid();
|
|
152
|
+
const intentId = uuid();
|
|
153
|
+
|
|
154
|
+
state.addGroup({
|
|
155
|
+
group_id: groupId,
|
|
156
|
+
driver: config.agent.id,
|
|
157
|
+
goal,
|
|
158
|
+
done_when: doneWhen,
|
|
159
|
+
intent_id: intentId,
|
|
160
|
+
participants: participantIds,
|
|
161
|
+
status: "active",
|
|
162
|
+
idle_turns: 0,
|
|
163
|
+
created_at: new Date().toISOString(),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await mqtt.subscribeGroup(groupId);
|
|
167
|
+
await publishStatus(config, mqtt, groupId);
|
|
168
|
+
|
|
169
|
+
for (const pid of participantIds) {
|
|
170
|
+
await invites.sendDirectInvite(pid, groupId, goal, doneWhen);
|
|
171
|
+
log(`Invite sent to ${contacts.getNameByAgentId(pid) ?? pid}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return json({
|
|
175
|
+
group_id: groupId,
|
|
176
|
+
intent_id: intentId,
|
|
177
|
+
participants: participantIds,
|
|
178
|
+
status: "invites_sent",
|
|
179
|
+
message: `Coordination started. Waiting for ${participantIds.length} agent(s) to join.`,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// -----------------------------------------------------------------------
|
|
185
|
+
// agentlink_submit_job
|
|
186
|
+
// -----------------------------------------------------------------------
|
|
187
|
+
const submitJobTool = {
|
|
188
|
+
name: "agentlink_submit_job",
|
|
189
|
+
label: "Submit Job",
|
|
190
|
+
description:
|
|
191
|
+
"Send a specific task to another agent. Use this to request actions like checking a calendar, searching for restaurants, etc.",
|
|
192
|
+
parameters: SubmitJobSchema,
|
|
193
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
194
|
+
const groupId = params.group_id as string;
|
|
195
|
+
const group = state.getGroup(groupId);
|
|
196
|
+
if (!group) return json({ error: "Group not found or already closed" });
|
|
197
|
+
|
|
198
|
+
const correlationId = await jobs.submitJob({
|
|
199
|
+
groupId,
|
|
200
|
+
intentId: group.intent_id,
|
|
201
|
+
targetAgentId: params.target_agent as string | undefined,
|
|
202
|
+
capability: params.capability as string,
|
|
203
|
+
text: params.text as string,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
state.resetIdleTurns(groupId);
|
|
207
|
+
|
|
208
|
+
return json({
|
|
209
|
+
correlation_id: correlationId,
|
|
210
|
+
status: "requested",
|
|
211
|
+
message: `Job sent: ${params.capability}. Waiting for response (timeout: ${config.jobTimeoutMs / 1000}s).`,
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// -----------------------------------------------------------------------
|
|
217
|
+
// agentlink_invite_agent
|
|
218
|
+
// -----------------------------------------------------------------------
|
|
219
|
+
const inviteAgentTool = {
|
|
220
|
+
name: "agentlink_invite_agent",
|
|
221
|
+
label: "Invite Agent",
|
|
222
|
+
description:
|
|
223
|
+
"Invite someone to join a coordination group. Use when adding new participants mid-coordination.",
|
|
224
|
+
parameters: InviteAgentSchema,
|
|
225
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
226
|
+
const groupId = params.group_id as string;
|
|
227
|
+
const group = state.getGroup(groupId);
|
|
228
|
+
if (!group) return json({ error: "Group not found" });
|
|
229
|
+
|
|
230
|
+
const nameOrId = params.name_or_agent_id as string;
|
|
231
|
+
const agentId = contacts.resolve(nameOrId);
|
|
232
|
+
if (!agentId) {
|
|
233
|
+
return json({ error: `Unknown contact: ${nameOrId}. Ask the user for their agent ID.` });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await invites.sendDirectInvite(agentId, groupId, group.goal, group.done_when);
|
|
237
|
+
log(`Invite sent to ${nameOrId}`);
|
|
238
|
+
|
|
239
|
+
return json({
|
|
240
|
+
group_id: groupId,
|
|
241
|
+
invited: agentId,
|
|
242
|
+
status: "invite_sent",
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// -----------------------------------------------------------------------
|
|
248
|
+
// agentlink_join_group
|
|
249
|
+
// -----------------------------------------------------------------------
|
|
250
|
+
const joinGroupTool = {
|
|
251
|
+
name: "agentlink_join_group",
|
|
252
|
+
label: "Join Group",
|
|
253
|
+
description: "Join a coordination group using an invite code that was shared with you.",
|
|
254
|
+
parameters: JoinGroupSchema,
|
|
255
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
256
|
+
const code = params.invite_code as string;
|
|
257
|
+
const invite = await invites.resolveInviteCode(code);
|
|
258
|
+
if (!invite) return json({ error: "Invalid or expired invite code" });
|
|
259
|
+
|
|
260
|
+
const groupId = invite.group_id;
|
|
261
|
+
|
|
262
|
+
await mqtt.subscribeGroup(groupId);
|
|
263
|
+
|
|
264
|
+
state.addGroup({
|
|
265
|
+
group_id: groupId,
|
|
266
|
+
driver: invite.from,
|
|
267
|
+
goal: invite.goal,
|
|
268
|
+
done_when: "",
|
|
269
|
+
intent_id: "",
|
|
270
|
+
participants: [invite.from],
|
|
271
|
+
status: "active",
|
|
272
|
+
idle_turns: 0,
|
|
273
|
+
created_at: new Date().toISOString(),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await publishStatus(config, mqtt, groupId);
|
|
277
|
+
|
|
278
|
+
const joinMsg = createEnvelope(config.agent.id, {
|
|
279
|
+
group_id: groupId,
|
|
280
|
+
to: "group",
|
|
281
|
+
type: "join",
|
|
282
|
+
payload: { text: `${config.agent.id} joined the group` },
|
|
283
|
+
});
|
|
284
|
+
await mqtt.publishEnvelope(TOPICS.groupSystem(groupId), joinMsg);
|
|
285
|
+
|
|
286
|
+
if (!contacts.resolve(invite.from)) {
|
|
287
|
+
contacts.add(invite.from, invite.from);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return json({
|
|
291
|
+
group_id: groupId,
|
|
292
|
+
driver: invite.from,
|
|
293
|
+
goal: invite.goal,
|
|
294
|
+
status: "joined",
|
|
295
|
+
});
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// -----------------------------------------------------------------------
|
|
300
|
+
// agentlink_complete
|
|
301
|
+
// -----------------------------------------------------------------------
|
|
302
|
+
const completeTool = {
|
|
303
|
+
name: "agentlink_complete",
|
|
304
|
+
label: "Complete",
|
|
305
|
+
description:
|
|
306
|
+
"Declare that the coordination is complete. Only call this when the goal has been achieved or explicitly abandoned.",
|
|
307
|
+
parameters: CompleteSchema,
|
|
308
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
309
|
+
const groupId = params.group_id as string;
|
|
310
|
+
const summary = params.summary as string;
|
|
311
|
+
const success = (params.success as boolean) ?? true;
|
|
312
|
+
|
|
313
|
+
const group = state.getGroup(groupId);
|
|
314
|
+
if (!group) return json({ error: "Group not found" });
|
|
315
|
+
|
|
316
|
+
if (group.driver !== config.agent.id) {
|
|
317
|
+
return json({ error: "Only the driver agent can complete a coordination" });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const completionMsg = createEnvelope(config.agent.id, {
|
|
321
|
+
group_id: groupId,
|
|
322
|
+
to: "group",
|
|
323
|
+
type: "leave",
|
|
324
|
+
payload: {
|
|
325
|
+
text: summary,
|
|
326
|
+
status: success ? "completed" : "failed",
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
await mqtt.publishEnvelope(TOPICS.groupSystem(groupId), completionMsg);
|
|
330
|
+
|
|
331
|
+
await mqtt.unsubscribeGroup(groupId);
|
|
332
|
+
|
|
333
|
+
await mqtt.publish(
|
|
334
|
+
TOPICS.groupStatus(groupId, config.agent.id),
|
|
335
|
+
"",
|
|
336
|
+
{ retain: true },
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
state.removeGroup(groupId);
|
|
340
|
+
|
|
341
|
+
log(`Coordination complete: ${summary}`);
|
|
342
|
+
|
|
343
|
+
return json({
|
|
344
|
+
group_id: groupId,
|
|
345
|
+
status: success ? "completed" : "failed",
|
|
346
|
+
summary,
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
return [coordinateTool, submitJobTool, inviteAgentTool, joinGroupTool, completeTool];
|
|
352
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { v4 as uuid } from "uuid";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Config types
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface AgentLinkConfig {
|
|
11
|
+
brokerUrl: string;
|
|
12
|
+
brokerUsername?: string;
|
|
13
|
+
brokerPassword?: string;
|
|
14
|
+
agent: {
|
|
15
|
+
id: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
capabilities: Capability[];
|
|
18
|
+
};
|
|
19
|
+
outputMode: "user" | "debug";
|
|
20
|
+
jobTimeoutMs: number;
|
|
21
|
+
dataDir: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Capability {
|
|
25
|
+
name: string;
|
|
26
|
+
tool: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
input_hint?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveConfig(rawConfig: Record<string, unknown>): AgentLinkConfig {
|
|
32
|
+
const cfg = rawConfig as Record<string, unknown>;
|
|
33
|
+
const agent = cfg.agent as Record<string, unknown> | undefined;
|
|
34
|
+
const capabilities = (agent?.capabilities as Capability[] | undefined) ?? [];
|
|
35
|
+
|
|
36
|
+
// Resolve dataDir first — needed for persistent agent ID lookup
|
|
37
|
+
const dataDir = (cfg.data_dir as string) ?? path.join(os.homedir(), ".agentlink");
|
|
38
|
+
|
|
39
|
+
// 3-tier agent ID resolution:
|
|
40
|
+
// 1. Explicit agent.id from config (user-configured)
|
|
41
|
+
// 2. Persistent agent_id from <dataDir>/state.json (set by CLI setup)
|
|
42
|
+
// 3. Temp fallback agent-HOSTNAME-PID (dev without setup)
|
|
43
|
+
let agentId = agent?.id as string | undefined;
|
|
44
|
+
if (!agentId) {
|
|
45
|
+
try {
|
|
46
|
+
const stateFile = path.join(dataDir, "state.json");
|
|
47
|
+
if (fs.existsSync(stateFile)) {
|
|
48
|
+
const stateData = JSON.parse(fs.readFileSync(stateFile, "utf-8"));
|
|
49
|
+
if (stateData.agent_id && typeof stateData.agent_id === "string") {
|
|
50
|
+
agentId = stateData.agent_id;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// state.json unreadable — fall through to temp ID
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!agentId) {
|
|
58
|
+
agentId = `agent-${os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, "")}-${process.pid}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
brokerUrl: (cfg.brokerUrl as string) ?? "mqtt://broker.emqx.io:1883",
|
|
63
|
+
brokerUsername: cfg.brokerUsername as string | undefined,
|
|
64
|
+
brokerPassword: cfg.brokerPassword as string | undefined,
|
|
65
|
+
agent: {
|
|
66
|
+
id: agentId,
|
|
67
|
+
description: agent?.description as string | undefined,
|
|
68
|
+
capabilities,
|
|
69
|
+
},
|
|
70
|
+
outputMode: (cfg.output_mode as "user" | "debug") ?? "user",
|
|
71
|
+
jobTimeoutMs: (cfg.job_timeout_ms as number) ?? 60_000,
|
|
72
|
+
dataDir,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Message envelope
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
export type MessageType = "chat" | "job_request" | "job_response" | "join" | "leave";
|
|
81
|
+
|
|
82
|
+
export type JobStatus = "requested" | "completed" | "failed" | "awaiting_approval";
|
|
83
|
+
|
|
84
|
+
export interface CoordinationHeader {
|
|
85
|
+
driver_agent_id: string;
|
|
86
|
+
goal: string;
|
|
87
|
+
done_when: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ProposalPayload {
|
|
91
|
+
summary: string;
|
|
92
|
+
requires_approval: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface MessagePayload {
|
|
96
|
+
text?: string;
|
|
97
|
+
capability?: string;
|
|
98
|
+
status?: JobStatus;
|
|
99
|
+
result?: string;
|
|
100
|
+
proposal?: ProposalPayload;
|
|
101
|
+
[key: string]: unknown;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface MessageEnvelope {
|
|
105
|
+
v: 1;
|
|
106
|
+
id: string;
|
|
107
|
+
group_id: string;
|
|
108
|
+
intent_id: string;
|
|
109
|
+
from: string;
|
|
110
|
+
to: "group" | string;
|
|
111
|
+
type: MessageType;
|
|
112
|
+
correlation_id?: string;
|
|
113
|
+
coordination?: CoordinationHeader;
|
|
114
|
+
payload: MessagePayload;
|
|
115
|
+
ts: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Agent status (retained message)
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
export interface CapabilityAdvertisement {
|
|
123
|
+
name: string;
|
|
124
|
+
description: string;
|
|
125
|
+
input_hint: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface AgentStatus {
|
|
129
|
+
agent_id: string;
|
|
130
|
+
owner: string;
|
|
131
|
+
status: "online" | "offline";
|
|
132
|
+
capabilities: CapabilityAdvertisement[];
|
|
133
|
+
description?: string;
|
|
134
|
+
ts: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Invite types
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
export interface InviteMessage {
|
|
142
|
+
type: "invite";
|
|
143
|
+
group_id: string;
|
|
144
|
+
from: string;
|
|
145
|
+
goal: string;
|
|
146
|
+
done_when: string;
|
|
147
|
+
ts: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface InviteCodePayload {
|
|
151
|
+
group_id: string;
|
|
152
|
+
from: string;
|
|
153
|
+
goal: string;
|
|
154
|
+
created_at: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// State types
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
export interface GroupState {
|
|
162
|
+
group_id: string;
|
|
163
|
+
driver: string;
|
|
164
|
+
goal: string;
|
|
165
|
+
done_when: string;
|
|
166
|
+
intent_id: string;
|
|
167
|
+
participants: string[];
|
|
168
|
+
status: "active" | "closing";
|
|
169
|
+
idle_turns: number;
|
|
170
|
+
created_at: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface PendingJob {
|
|
174
|
+
correlation_id: string;
|
|
175
|
+
group_id: string;
|
|
176
|
+
target: string;
|
|
177
|
+
capability: string;
|
|
178
|
+
status: JobStatus;
|
|
179
|
+
sent_at: string;
|
|
180
|
+
text?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface TimedOutJob extends PendingJob {
|
|
184
|
+
timed_out: true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Helper: message construction
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
export function createEnvelope(
|
|
192
|
+
from: string,
|
|
193
|
+
overrides: Partial<MessageEnvelope> & Pick<MessageEnvelope, "group_id" | "to" | "type" | "payload">,
|
|
194
|
+
): MessageEnvelope {
|
|
195
|
+
return {
|
|
196
|
+
v: 1,
|
|
197
|
+
id: uuid(),
|
|
198
|
+
intent_id: overrides.intent_id ?? uuid(),
|
|
199
|
+
from,
|
|
200
|
+
ts: new Date().toISOString(),
|
|
201
|
+
...overrides,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Topic helpers
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
export const TOPICS = {
|
|
210
|
+
inbox: (agentId: string) =>
|
|
211
|
+
`agentlink/agents/${agentId}/inbox`,
|
|
212
|
+
|
|
213
|
+
groupMessages: (groupId: string, agentId: string) =>
|
|
214
|
+
`agentlink/${groupId}/messages/${agentId}`,
|
|
215
|
+
|
|
216
|
+
groupMessagesWildcard: (groupId: string) =>
|
|
217
|
+
`agentlink/${groupId}/messages/+`,
|
|
218
|
+
|
|
219
|
+
groupStatus: (groupId: string, agentId: string) =>
|
|
220
|
+
`agentlink/${groupId}/status/${agentId}`,
|
|
221
|
+
|
|
222
|
+
groupStatusWildcard: (groupId: string) =>
|
|
223
|
+
`agentlink/${groupId}/status/+`,
|
|
224
|
+
|
|
225
|
+
groupSystem: (groupId: string) =>
|
|
226
|
+
`agentlink/${groupId}/system`,
|
|
227
|
+
|
|
228
|
+
groupAll: (groupId: string) =>
|
|
229
|
+
`agentlink/${groupId}/#`,
|
|
230
|
+
|
|
231
|
+
inviteCode: (code: string) =>
|
|
232
|
+
`agentlink/invites/${code}`,
|
|
233
|
+
} as const;
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Type guards
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
export function isInviteMessage(msg: unknown): msg is InviteMessage {
|
|
240
|
+
return (
|
|
241
|
+
typeof msg === "object" &&
|
|
242
|
+
msg !== null &&
|
|
243
|
+
(msg as Record<string, unknown>).type === "invite" &&
|
|
244
|
+
typeof (msg as Record<string, unknown>).group_id === "string" &&
|
|
245
|
+
typeof (msg as Record<string, unknown>).from === "string"
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function isMessageEnvelope(msg: unknown): msg is MessageEnvelope {
|
|
250
|
+
if (typeof msg !== "object" || msg === null) return false;
|
|
251
|
+
const m = msg as Record<string, unknown>;
|
|
252
|
+
return (
|
|
253
|
+
m.v === 1 &&
|
|
254
|
+
typeof m.id === "string" &&
|
|
255
|
+
typeof m.from === "string" &&
|
|
256
|
+
typeof m.type === "string"
|
|
257
|
+
);
|
|
258
|
+
}
|