@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/channel.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { AgentLinkConfig, MessageEnvelope } from "./types.js";
|
|
2
|
+
import { createEnvelope, TOPICS } from "./types.js";
|
|
3
|
+
import type { StateManager } from "./state.js";
|
|
4
|
+
import type { ContactsManager } from "./contacts.js";
|
|
5
|
+
import type { MqttService } from "./mqtt-service.js";
|
|
6
|
+
import type { Logger } from "./mqtt-client.js";
|
|
7
|
+
|
|
8
|
+
export interface ChannelPlugin {
|
|
9
|
+
id: string;
|
|
10
|
+
meta: {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
selectionLabel: string;
|
|
14
|
+
blurb: string;
|
|
15
|
+
aliases: string[];
|
|
16
|
+
};
|
|
17
|
+
capabilities: {
|
|
18
|
+
chatTypes: string[];
|
|
19
|
+
};
|
|
20
|
+
config: {
|
|
21
|
+
listAccountIds: () => string[];
|
|
22
|
+
resolveAccount: () => { accountId: string; enabled: boolean; configured: boolean };
|
|
23
|
+
};
|
|
24
|
+
outbound: {
|
|
25
|
+
deliveryMode: string;
|
|
26
|
+
sendText: (params: {
|
|
27
|
+
text: string;
|
|
28
|
+
threadId?: string;
|
|
29
|
+
channelId?: string;
|
|
30
|
+
}) => Promise<{ ok: boolean; error?: string }>;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createChannelPlugin(
|
|
35
|
+
config: AgentLinkConfig,
|
|
36
|
+
state: StateManager,
|
|
37
|
+
contacts: ContactsManager,
|
|
38
|
+
mqtt: MqttService,
|
|
39
|
+
logger: Logger,
|
|
40
|
+
): ChannelPlugin {
|
|
41
|
+
return {
|
|
42
|
+
id: "agentlink",
|
|
43
|
+
meta: {
|
|
44
|
+
id: "agentlink",
|
|
45
|
+
label: "AgentLink",
|
|
46
|
+
selectionLabel: "AgentLink (Agent Coordination)",
|
|
47
|
+
blurb: "Agent-to-agent coordination channel",
|
|
48
|
+
aliases: ["al"],
|
|
49
|
+
},
|
|
50
|
+
capabilities: {
|
|
51
|
+
chatTypes: ["direct", "group"],
|
|
52
|
+
},
|
|
53
|
+
config: {
|
|
54
|
+
listAccountIds: () => [config.agent.id],
|
|
55
|
+
resolveAccount: () => ({
|
|
56
|
+
accountId: config.agent.id,
|
|
57
|
+
enabled: true,
|
|
58
|
+
configured: true,
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
outbound: {
|
|
62
|
+
deliveryMode: "direct",
|
|
63
|
+
async sendText({ text, threadId, channelId }) {
|
|
64
|
+
if (!threadId) return { ok: false, error: "No group context" };
|
|
65
|
+
|
|
66
|
+
const group = state.getGroup(threadId);
|
|
67
|
+
if (!group) return { ok: false, error: "Group not found" };
|
|
68
|
+
|
|
69
|
+
const envelope = createEnvelope(config.agent.id, {
|
|
70
|
+
group_id: threadId,
|
|
71
|
+
intent_id: group.intent_id,
|
|
72
|
+
to: channelId ?? "group",
|
|
73
|
+
type: "chat",
|
|
74
|
+
payload: { text },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await mqtt.publishEnvelope(
|
|
78
|
+
TOPICS.groupMessages(threadId, config.agent.id),
|
|
79
|
+
envelope,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Track idle turns for anti-deadlock (chat without job/proposal increments)
|
|
83
|
+
if (group.driver === config.agent.id) {
|
|
84
|
+
state.incrementIdleTurns(threadId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { ok: true };
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
package/src/contacts.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface ContactEntry {
|
|
5
|
+
agent_id: string;
|
|
6
|
+
added: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ContactsManager {
|
|
10
|
+
resolve(nameOrId: string): string | null;
|
|
11
|
+
add(name: string, agentId: string): void;
|
|
12
|
+
remove(name: string): void;
|
|
13
|
+
has(name: string): boolean;
|
|
14
|
+
getAll(): Record<string, ContactEntry>;
|
|
15
|
+
getNameByAgentId(agentId: string): string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createContacts(dataDir: string): ContactsManager {
|
|
19
|
+
const filePath = path.join(dataDir, "contacts.json");
|
|
20
|
+
let contacts: Record<string, ContactEntry> = {};
|
|
21
|
+
|
|
22
|
+
if (fs.existsSync(filePath)) {
|
|
23
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
24
|
+
contacts = raw.contacts ?? {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function save() {
|
|
28
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
29
|
+
fs.writeFileSync(filePath, JSON.stringify({ contacts }, null, 2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
resolve(nameOrId) {
|
|
34
|
+
if (contacts[nameOrId]) return contacts[nameOrId].agent_id;
|
|
35
|
+
for (const entry of Object.values(contacts)) {
|
|
36
|
+
if (entry.agent_id === nameOrId) return nameOrId;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
add(name, agentId) {
|
|
42
|
+
contacts[name] = {
|
|
43
|
+
agent_id: agentId,
|
|
44
|
+
added: new Date().toISOString().split("T")[0],
|
|
45
|
+
};
|
|
46
|
+
save();
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
remove(name) {
|
|
50
|
+
delete contacts[name];
|
|
51
|
+
save();
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
has(name) {
|
|
55
|
+
return name in contacts;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
getAll() {
|
|
59
|
+
return { ...contacts };
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
getNameByAgentId(agentId) {
|
|
63
|
+
for (const [name, entry] of Object.entries(contacts)) {
|
|
64
|
+
if (entry.agent_id === agentId) return name;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { AgentLinkConfig } from "./types.js";
|
|
2
|
+
import { resolveConfig } from "./types.js";
|
|
3
|
+
import { createContacts } from "./contacts.js";
|
|
4
|
+
import { createState } from "./state.js";
|
|
5
|
+
import { createMqttService } from "./mqtt-service.js";
|
|
6
|
+
import { createInviteManager } from "./invite.js";
|
|
7
|
+
import { createJobManager } from "./jobs.js";
|
|
8
|
+
import { createTools } from "./tools.js";
|
|
9
|
+
import { createChannelPlugin } from "./channel.js";
|
|
10
|
+
import { shouldProcess } from "./routing.js";
|
|
11
|
+
import type { Logger } from "./mqtt-client.js";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Minimal OC Plugin API type (matches what we use from OpenClaw's PluginApi)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
interface PluginApi {
|
|
18
|
+
config: Record<string, unknown>;
|
|
19
|
+
pluginConfig?: Record<string, unknown>;
|
|
20
|
+
logger: Logger;
|
|
21
|
+
registerService(service: { id: string; start: () => Promise<void>; stop: () => Promise<void> }): void;
|
|
22
|
+
registerTool(tool: {
|
|
23
|
+
name: string;
|
|
24
|
+
label: string;
|
|
25
|
+
description: string;
|
|
26
|
+
parameters: unknown;
|
|
27
|
+
execute: (_id: string, params: Record<string, unknown>) => Promise<{
|
|
28
|
+
content: Array<{ type: "text"; text: string }>;
|
|
29
|
+
details: unknown;
|
|
30
|
+
}>;
|
|
31
|
+
}): void;
|
|
32
|
+
registerChannel?(registration: { plugin: unknown }): void;
|
|
33
|
+
registerCli?(registrar: (ctx: { program: unknown }) => void, opts?: { commands?: string[] }): void;
|
|
34
|
+
on?(hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }): void;
|
|
35
|
+
runtime?: {
|
|
36
|
+
executeTool(params: {
|
|
37
|
+
toolName: string;
|
|
38
|
+
params: Record<string, unknown>;
|
|
39
|
+
ctx?: unknown;
|
|
40
|
+
}): Promise<unknown>;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Plugin definition object (preferred OC pattern)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export default {
|
|
49
|
+
id: "agentlink",
|
|
50
|
+
name: "AgentLink",
|
|
51
|
+
description: "Agent-to-agent coordination over MQTT",
|
|
52
|
+
register,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function register(api: PluginApi) {
|
|
56
|
+
const config = resolveConfig(api.pluginConfig ?? {});
|
|
57
|
+
const contacts = createContacts(config.dataDir);
|
|
58
|
+
const state = createState(config.dataDir);
|
|
59
|
+
const logger = api.logger;
|
|
60
|
+
|
|
61
|
+
function log(msg: string) {
|
|
62
|
+
if (config.outputMode === "debug") {
|
|
63
|
+
logger.info(`[AgentLink] ${msg}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const mqttService = createMqttService(config, state, contacts, logger);
|
|
68
|
+
|
|
69
|
+
const invites = createInviteManager(config, mqttService);
|
|
70
|
+
|
|
71
|
+
// executeTool bridge: calls the local OC tool when a job_request arrives
|
|
72
|
+
const executeTool = api.runtime
|
|
73
|
+
? async (toolId: string, input: string): Promise<string> => {
|
|
74
|
+
const result = await api.runtime!.executeTool({
|
|
75
|
+
toolName: toolId,
|
|
76
|
+
params: { input },
|
|
77
|
+
});
|
|
78
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
79
|
+
}
|
|
80
|
+
: undefined;
|
|
81
|
+
|
|
82
|
+
const jobs = createJobManager(config, state, mqttService, logger, executeTool);
|
|
83
|
+
|
|
84
|
+
// Wire inbound message handling
|
|
85
|
+
mqttService.onGroupMessage((msg) => {
|
|
86
|
+
// Check if this agent should process the message
|
|
87
|
+
if (!shouldProcess(msg, config.agent.id, config.agent.capabilities)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (msg.type === "job_request") {
|
|
92
|
+
jobs.handleJobRequest(msg);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (msg.type === "job_response" && msg.correlation_id && state.hasPendingJob(msg.correlation_id)) {
|
|
97
|
+
jobs.handleJobResponse(msg);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// General coordination message — log it
|
|
102
|
+
const senderName = contacts.getNameByAgentId(msg.from) ?? msg.from;
|
|
103
|
+
log(`Message from ${senderName}: ${msg.payload.text ?? "(no text)"}`);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
mqttService.onInboxMessage((_topic, raw) => {
|
|
107
|
+
const msg = raw as Record<string, unknown>;
|
|
108
|
+
if (msg.type === "invite") {
|
|
109
|
+
log(`Invite received from ${msg.from}: "${msg.goal}"`);
|
|
110
|
+
// For V1: auto-join if from known contact
|
|
111
|
+
const fromId = msg.from as string;
|
|
112
|
+
if (contacts.resolve(fromId)) {
|
|
113
|
+
log(`Auto-joining (known contact: ${contacts.getNameByAgentId(fromId) ?? fromId})`);
|
|
114
|
+
// Auto-join logic handled by the agent's LLM calling agentlink_join_group
|
|
115
|
+
// or we could auto-accept here for known contacts
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
mqttService.onSystemEvent((raw) => {
|
|
121
|
+
const msg = raw as Record<string, unknown>;
|
|
122
|
+
if (msg && typeof msg === "object" && "type" in msg) {
|
|
123
|
+
const envelope = msg as Record<string, unknown>;
|
|
124
|
+
if (envelope.type === "join") {
|
|
125
|
+
const from = envelope.from as string;
|
|
126
|
+
const groupId = envelope.group_id as string;
|
|
127
|
+
const group = state.getGroup(groupId);
|
|
128
|
+
if (group && !group.participants.includes(from)) {
|
|
129
|
+
group.participants.push(from);
|
|
130
|
+
state.updateGroup(groupId, { participants: group.participants });
|
|
131
|
+
}
|
|
132
|
+
log(`${contacts.getNameByAgentId(from) ?? from} joined group ${groupId}`);
|
|
133
|
+
}
|
|
134
|
+
if (envelope.type === "leave") {
|
|
135
|
+
const groupId = envelope.group_id as string;
|
|
136
|
+
// If this is a completion message from the driver, clean up
|
|
137
|
+
const group = state.getGroup(groupId);
|
|
138
|
+
if (group && envelope.from !== config.agent.id) {
|
|
139
|
+
mqttService.unsubscribeGroup(groupId);
|
|
140
|
+
state.removeGroup(groupId);
|
|
141
|
+
log(`Group ${groupId} closed by driver`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Background MQTT connection
|
|
148
|
+
api.registerService({
|
|
149
|
+
id: "agentlink-mqtt",
|
|
150
|
+
start: async () => {
|
|
151
|
+
await mqttService.start();
|
|
152
|
+
|
|
153
|
+
// Process pending joins from CLI --join flag
|
|
154
|
+
const pendingJoins = state.getPendingJoins();
|
|
155
|
+
for (const code of pendingJoins) {
|
|
156
|
+
try {
|
|
157
|
+
log(`Processing pending join: ${code}`);
|
|
158
|
+
const invite = await invites.resolveInviteCode(code);
|
|
159
|
+
if (invite) {
|
|
160
|
+
const groupId = invite.group_id;
|
|
161
|
+
await mqttService.subscribeGroup(groupId);
|
|
162
|
+
state.addGroup({
|
|
163
|
+
group_id: groupId,
|
|
164
|
+
driver: invite.from,
|
|
165
|
+
goal: invite.goal,
|
|
166
|
+
done_when: "",
|
|
167
|
+
intent_id: "",
|
|
168
|
+
participants: [invite.from],
|
|
169
|
+
status: "active",
|
|
170
|
+
idle_turns: 0,
|
|
171
|
+
created_at: new Date().toISOString(),
|
|
172
|
+
});
|
|
173
|
+
if (!contacts.resolve(invite.from)) {
|
|
174
|
+
contacts.add(invite.from, invite.from);
|
|
175
|
+
}
|
|
176
|
+
state.removePendingJoin(code);
|
|
177
|
+
log(`Auto-joined group from setup: ${code}`);
|
|
178
|
+
} else {
|
|
179
|
+
log(`Pending join ${code}: invite not found (will retry next restart)`);
|
|
180
|
+
}
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
183
|
+
log(`Failed to auto-join ${code}: ${msg}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
stop: () => mqttService.stop(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Register the 5 agent tools
|
|
191
|
+
const tools = createTools(config, state, contacts, mqttService, invites, jobs, logger);
|
|
192
|
+
for (const tool of tools) {
|
|
193
|
+
api.registerTool(tool);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Optional channel registration
|
|
197
|
+
if (api.registerChannel) {
|
|
198
|
+
const channelPlugin = createChannelPlugin(config, state, contacts, mqttService, logger);
|
|
199
|
+
api.registerChannel({ plugin: channelPlugin });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Anti-deadlock system prompt injection
|
|
203
|
+
if (api.on) {
|
|
204
|
+
api.on("before_prompt_build", () => {
|
|
205
|
+
const activeGroups = state.getActiveGroups();
|
|
206
|
+
if (activeGroups.length === 0) return {};
|
|
207
|
+
|
|
208
|
+
const driverGroups = activeGroups
|
|
209
|
+
.map((id) => state.getGroup(id))
|
|
210
|
+
.filter((g) => g?.driver === config.agent.id);
|
|
211
|
+
|
|
212
|
+
if (driverGroups.length === 0) return {};
|
|
213
|
+
|
|
214
|
+
let prompt = "\n\n## AgentLink Coordination Rules (MANDATORY)\n";
|
|
215
|
+
prompt += "You are the DRIVER of active coordination(s). You MUST follow these rules:\n";
|
|
216
|
+
prompt += "1. Issue direct jobs (agentlink_submit_job) instead of asking open-ended questions.\n";
|
|
217
|
+
prompt += "2. Make concrete proposals with specifics (time, place, price), not vague suggestions.\n";
|
|
218
|
+
prompt += "3. After 3 turns of discussion without a job, proposal, or completion, you MUST force progress.\n";
|
|
219
|
+
prompt += "4. Declare completion (agentlink_complete) as soon as the done_when condition is met.\n";
|
|
220
|
+
prompt += "5. Hub-and-spoke: you mediate all coordination. Participants respond to you, not each other.\n\n";
|
|
221
|
+
|
|
222
|
+
for (const group of driverGroups) {
|
|
223
|
+
if (!group) continue;
|
|
224
|
+
prompt += `Active: "${group.goal}" | Done when: "${group.done_when}" | Idle turns: ${group.idle_turns}/3\n`;
|
|
225
|
+
if (group.idle_turns >= 3) {
|
|
226
|
+
prompt += ` WARNING: 3 idle turns reached. You MUST take concrete action NOW.\n`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { appendSystemContext: prompt };
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// CLI commands
|
|
235
|
+
if (api.registerCli) {
|
|
236
|
+
api.registerCli(
|
|
237
|
+
({ program }: { program: any }) => {
|
|
238
|
+
const cmd = program.command("agentlink").description("AgentLink agent coordination");
|
|
239
|
+
cmd
|
|
240
|
+
.command("status")
|
|
241
|
+
.description("Show AgentLink connection and group status")
|
|
242
|
+
.action(() => {
|
|
243
|
+
console.log(`Agent ID: ${config.agent.id}`);
|
|
244
|
+
console.log(`Broker: ${config.brokerUrl}`);
|
|
245
|
+
console.log(`Connected: ${mqttService.getClient().isConnected()}`);
|
|
246
|
+
console.log(`Active groups: ${state.getActiveGroups().length}`);
|
|
247
|
+
console.log(
|
|
248
|
+
`Capabilities: ${config.agent.capabilities.map((c) => c.name).join(", ") || "none"}`,
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
cmd
|
|
252
|
+
.command("contacts")
|
|
253
|
+
.description("List known contacts")
|
|
254
|
+
.action(() => {
|
|
255
|
+
const all = contacts.getAll();
|
|
256
|
+
const entries = Object.entries(all);
|
|
257
|
+
if (entries.length === 0) {
|
|
258
|
+
console.log("No contacts.");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
for (const [name, entry] of entries) {
|
|
262
|
+
console.log(`${name} -> ${entry.agent_id} (added ${entry.added})`);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
{ commands: ["agentlink"] },
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
package/src/invite.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { v4 as uuid } from "uuid";
|
|
2
|
+
import type { AgentLinkConfig, InviteMessage, InviteCodePayload } from "./types.js";
|
|
3
|
+
import { TOPICS } from "./types.js";
|
|
4
|
+
import type { MqttService } from "./mqtt-service.js";
|
|
5
|
+
|
|
6
|
+
export interface InviteCodeResult {
|
|
7
|
+
code: string;
|
|
8
|
+
shareableMessage: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface InviteManager {
|
|
12
|
+
createInviteCode(groupId: string, from: string, goal: string): Promise<InviteCodeResult>;
|
|
13
|
+
resolveInviteCode(code: string): Promise<InviteCodePayload | null>;
|
|
14
|
+
sendDirectInvite(targetAgentId: string, groupId: string, goal: string, doneWhen: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createInviteManager(
|
|
18
|
+
config: AgentLinkConfig,
|
|
19
|
+
mqtt: MqttService,
|
|
20
|
+
): InviteManager {
|
|
21
|
+
return {
|
|
22
|
+
async createInviteCode(groupId, from, goal) {
|
|
23
|
+
const code = uuid().replace(/-/g, "").substring(0, 6).toUpperCase();
|
|
24
|
+
|
|
25
|
+
const payload: InviteCodePayload = {
|
|
26
|
+
group_id: groupId,
|
|
27
|
+
from,
|
|
28
|
+
goal,
|
|
29
|
+
created_at: new Date().toISOString(),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
await mqtt.publish(
|
|
33
|
+
TOPICS.inviteCode(code),
|
|
34
|
+
JSON.stringify(payload),
|
|
35
|
+
{ retain: true, qos: 1 },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const shareableMessage = [
|
|
39
|
+
`Join my agent coordination: ${code}`,
|
|
40
|
+
`1. Install AgentLink: openclaw plugins install @agentlinkdev/openclaw`,
|
|
41
|
+
`2. Tell your agent: "Join AgentLink group ${code}"`,
|
|
42
|
+
].join("\n");
|
|
43
|
+
|
|
44
|
+
return { code, shareableMessage };
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async resolveInviteCode(code) {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const topic = TOPICS.inviteCode(code);
|
|
50
|
+
let resolved = false;
|
|
51
|
+
|
|
52
|
+
const timeout = setTimeout(() => {
|
|
53
|
+
if (!resolved) {
|
|
54
|
+
resolved = true;
|
|
55
|
+
resolve(null);
|
|
56
|
+
}
|
|
57
|
+
}, 5000);
|
|
58
|
+
|
|
59
|
+
const handler = (msgTopic: string, payload: Buffer) => {
|
|
60
|
+
if (msgTopic === topic && !resolved) {
|
|
61
|
+
resolved = true;
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
try {
|
|
64
|
+
resolve(JSON.parse(payload.toString()));
|
|
65
|
+
} catch {
|
|
66
|
+
resolve(null);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
mqtt.getClient().onMessage(handler);
|
|
72
|
+
mqtt.getClient().subscribe(topic).catch(() => {
|
|
73
|
+
if (!resolved) {
|
|
74
|
+
resolved = true;
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
resolve(null);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async sendDirectInvite(targetAgentId, groupId, goal, doneWhen) {
|
|
83
|
+
const invite: InviteMessage = {
|
|
84
|
+
type: "invite",
|
|
85
|
+
group_id: groupId,
|
|
86
|
+
from: config.agent.id,
|
|
87
|
+
goal,
|
|
88
|
+
done_when: doneWhen,
|
|
89
|
+
ts: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
await mqtt.publish(
|
|
93
|
+
TOPICS.inbox(targetAgentId),
|
|
94
|
+
JSON.stringify(invite),
|
|
95
|
+
{ qos: 1 },
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/jobs.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { v4 as uuid } from "uuid";
|
|
2
|
+
import type { AgentLinkConfig, MessageEnvelope } from "./types.js";
|
|
3
|
+
import { createEnvelope, TOPICS } from "./types.js";
|
|
4
|
+
import type { StateManager } from "./state.js";
|
|
5
|
+
import type { MqttService } from "./mqtt-service.js";
|
|
6
|
+
import type { Logger } from "./mqtt-client.js";
|
|
7
|
+
|
|
8
|
+
export interface SubmitJobParams {
|
|
9
|
+
groupId: string;
|
|
10
|
+
intentId: string;
|
|
11
|
+
targetAgentId?: string;
|
|
12
|
+
capability: string;
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface JobManager {
|
|
17
|
+
submitJob(params: SubmitJobParams): Promise<string>;
|
|
18
|
+
handleJobResponse(msg: MessageEnvelope): void;
|
|
19
|
+
handleJobRequest(msg: MessageEnvelope): Promise<MessageEnvelope | null>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createJobManager(
|
|
23
|
+
config: AgentLinkConfig,
|
|
24
|
+
state: StateManager,
|
|
25
|
+
mqtt: MqttService,
|
|
26
|
+
logger: Logger,
|
|
27
|
+
executeTool?: (toolId: string, input: string) => Promise<string>,
|
|
28
|
+
): JobManager {
|
|
29
|
+
return {
|
|
30
|
+
async submitJob(params) {
|
|
31
|
+
const correlationId = uuid();
|
|
32
|
+
const envelope = createEnvelope(config.agent.id, {
|
|
33
|
+
group_id: params.groupId,
|
|
34
|
+
intent_id: params.intentId,
|
|
35
|
+
to: params.targetAgentId ?? "group",
|
|
36
|
+
type: "job_request",
|
|
37
|
+
correlation_id: correlationId,
|
|
38
|
+
payload: {
|
|
39
|
+
text: params.text,
|
|
40
|
+
capability: params.capability,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
state.addJob({
|
|
45
|
+
correlation_id: correlationId,
|
|
46
|
+
group_id: params.groupId,
|
|
47
|
+
target: params.targetAgentId ?? "group",
|
|
48
|
+
capability: params.capability,
|
|
49
|
+
status: "requested",
|
|
50
|
+
sent_at: envelope.ts,
|
|
51
|
+
text: params.text,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await mqtt.publishEnvelope(
|
|
55
|
+
TOPICS.groupMessages(params.groupId, config.agent.id),
|
|
56
|
+
envelope,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Start timeout timer
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
if (state.hasPendingJob(correlationId)) {
|
|
62
|
+
state.completeJob(correlationId, "failed");
|
|
63
|
+
logger.info(`[AgentLink] Job ${correlationId} timed out (${params.capability})`);
|
|
64
|
+
}
|
|
65
|
+
}, config.jobTimeoutMs);
|
|
66
|
+
|
|
67
|
+
return correlationId;
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
handleJobResponse(msg) {
|
|
71
|
+
if (!msg.correlation_id) return;
|
|
72
|
+
const job = state.getJob(msg.correlation_id);
|
|
73
|
+
if (!job) return;
|
|
74
|
+
|
|
75
|
+
const status = msg.payload.status ?? "completed";
|
|
76
|
+
state.completeJob(msg.correlation_id, status);
|
|
77
|
+
logger.info(
|
|
78
|
+
`[AgentLink] Job ${msg.correlation_id} ${status}: ${msg.payload.result ?? "(no result)"}`,
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async handleJobRequest(msg) {
|
|
83
|
+
const capability = msg.payload.capability;
|
|
84
|
+
if (!capability) return null;
|
|
85
|
+
|
|
86
|
+
const cap = config.agent.capabilities.find((c) => c.name === capability);
|
|
87
|
+
if (!cap) {
|
|
88
|
+
const response = createEnvelope(config.agent.id, {
|
|
89
|
+
group_id: msg.group_id,
|
|
90
|
+
intent_id: msg.intent_id,
|
|
91
|
+
to: msg.from,
|
|
92
|
+
type: "job_response",
|
|
93
|
+
correlation_id: msg.correlation_id,
|
|
94
|
+
payload: {
|
|
95
|
+
status: "failed",
|
|
96
|
+
result: `Capability '${capability}' not available`,
|
|
97
|
+
capability,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
await mqtt.publishEnvelope(
|
|
101
|
+
TOPICS.groupMessages(msg.group_id, config.agent.id),
|
|
102
|
+
response,
|
|
103
|
+
);
|
|
104
|
+
return response;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
logger.info(`[AgentLink] Running local tool: ${cap.tool} for capability: ${capability}`);
|
|
108
|
+
|
|
109
|
+
let result: string;
|
|
110
|
+
try {
|
|
111
|
+
if (executeTool) {
|
|
112
|
+
result = await executeTool(cap.tool, msg.payload.text ?? "");
|
|
113
|
+
} else {
|
|
114
|
+
result = `Tool execution not available (no executeTool provided)`;
|
|
115
|
+
}
|
|
116
|
+
} catch (err: unknown) {
|
|
117
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
118
|
+
const response = createEnvelope(config.agent.id, {
|
|
119
|
+
group_id: msg.group_id,
|
|
120
|
+
intent_id: msg.intent_id,
|
|
121
|
+
to: msg.from,
|
|
122
|
+
type: "job_response",
|
|
123
|
+
correlation_id: msg.correlation_id,
|
|
124
|
+
payload: {
|
|
125
|
+
status: "failed",
|
|
126
|
+
result: `Tool execution error: ${errMsg}`,
|
|
127
|
+
capability,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
await mqtt.publishEnvelope(
|
|
131
|
+
TOPICS.groupMessages(msg.group_id, config.agent.id),
|
|
132
|
+
response,
|
|
133
|
+
);
|
|
134
|
+
return response;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const response = createEnvelope(config.agent.id, {
|
|
138
|
+
group_id: msg.group_id,
|
|
139
|
+
intent_id: msg.intent_id,
|
|
140
|
+
to: msg.from,
|
|
141
|
+
type: "job_response",
|
|
142
|
+
correlation_id: msg.correlation_id,
|
|
143
|
+
payload: {
|
|
144
|
+
status: "completed",
|
|
145
|
+
result,
|
|
146
|
+
capability,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
await mqtt.publishEnvelope(
|
|
150
|
+
TOPICS.groupMessages(msg.group_id, config.agent.id),
|
|
151
|
+
response,
|
|
152
|
+
);
|
|
153
|
+
return response;
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|